あぼぼーぼ・ぼーぼぼ

のんびり生きたい

『LEADER’s KPT』を読んだ

www.amazon.co.jp

KPTとは何か?どうやるか?についての入門的な内容がまとめられていました。

すでにKPTに慣れ親しんだ自分にとっては目新しい発見こそ少なかったですが、つまらなかったわけでもないです。例えばKeep/Problem/Tryの数や比率を継続的に計測してチームの状態を把握する、みたいな方法は、繰り返し行われることの多いKPTと相性が良く合理的で良いなぁと思いました。

最近読んでいる本と違って、紹介されている理論の解説が薄かったり、参考文献が(ほとんど)出てこないような本なので、よく言えばすごくライトに読めました。

Goのリンターstaticcheckのルールを全部読んだからいくつか紹介

Goのリンターの1つであるstaticcheckのルールを全部読みました。

staticcheck.io

全部書くとただのコピーになってしまうので、その中からかいつまんでいくつか紹介します。

SA1 – Various misuses of the standard library

SA1004 - Suspiciously small untyped constant in time.Sleep

time.Sleep関数に渡されたtime.Durationはナノ秒単位なので、あまりにも小さい値の場合はバグの原因として注意してくれるようです。他の言語だとこういうAPIの場合ミリ秒とかだったりしますからね。意図的に小さい値を使いたい場合はtime.NanosecondeをかければOK。

time.Sleep(1) // NG
// main.go:19:13: sleeping for 1 nanoseconds is probably a bug; be explicit if it isn't (SA1004)

time.Sleep(1 * time.Nanosecond) // OK

SA1005 - Invalid first argument to exec.Command

exec.Command関数の第一引数がシェルコマンドだったらその次の引数がプログラム名やパスじゃないと注意してくれる。ほぇ〜。ただ第二引数以降に入ってる場合は注意なし。

exec.Command("ls arg1") // NG
// main.go:8:15: first argument to exec.Command looks like a shell command, but a program name or path are expected (SA1005)

exec.Command("ls ./") // OK

SA1006 - Printf with dynamic first argument and no further arguments

ユーザー入力などの動的な文字列をフォーマットとしてfmt.Printf関数に渡すのを注意してくれる。この例だと別のルール(SA5009)にも引っ掛かりますね。

s := "Interest rate: 5%"
fmt.Printf(s) // NG
// main.go:9:2: printf-style function with dynamic format string and no further arguments should use print-style function instead (SA1006)
// main.go:9:13: couldn't parse format string (SA5009)

fmt.Printf("%s", s) // OK

SA1008 - Non-canonical key in http.Header map

http.Headerのキーは大文字始まりで正規化されていて、http.Header.Addのような用意されている関数経由ならそうなるが、mapに直接追加するなどするとそうならないので注意してくれる...って書いてあるけど手元で試してみたところ引っかからなかった。

追加時ではなく参照するコードに対してチェックしているようだった。

h := http.Header{}
h["etag"] = []string{"1234"} // staticcheckでは怒られない
h.Add("etag", "5678")
// map[Etag:[5678] etag:[1234]]

_ = h["etag"] // NG
// main.go:12:6: keys in http.Header are canonicalized, "etag" is not canonical; fix the constant or use http.CanonicalHeaderKey (SA1008)

_ = http.CanonicalHeaderKey("etag") // OK -> "Etag"

SA1019 - Using a deprecated function, variable, constant or field

deprecatedになったAPIの利用を注意してくれる。すごくありがたいけどどうやって検知してるんだろう?と思ってtestdataコードを読むと各バージョンごとにルールを作ってるみたいでした。

https://github.com/dominikh/go-tools/blob/master/staticcheck/testdata/src/CheckDeprecated_go119/CheckDeprecated.go

SA1024 - A string cutset contains duplicate characters

strings.TrimLeftとstrings.TrimPrefixの違いが分かる。strings.TrimLeftやTrimRightは第二引数に取り除きたい文字のセットを渡すので、重複がいらない。

fmt.Println(strings.TrimLeft("42133word", "12334")) // NG
// word
// main.go:10:44: cutset contains duplicate characters (SA1024)

fmt.Println(strings.TrimLeft("42133word", "1234")) // OK
// word

SA1028 - sort.Slice can only be used on slices

sort.Slice関数の第一引数xはanyなんですが、GoDocにもあるとおりスライスじゃないとpanicしてしまう。こういうのを注意してくれるのは助かりますね。

https://pkg.go.dev/sort#Slice

SA2 – Concurrency issues

SA2001 - Empty critical section, did you mean to defer the unlock?

Lock直後に書かれたUnlockにdeferが付いてなければ注意してくれる。ただそれが有用な場合もあるとのことで、追々理解したい。

SA4 – Code that isn't really doing anything

_人人人人人人人人人人人人_
> 何もしていないコード <
 ̄Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^ ̄

SA4系のルールは全てありがたいと思いました。

SA4023 - Impossible comparison of interface value with untyped nil

ほぇぇとなったやつ。インタフェース変数にnil値を格納するとインタフェース変数はnilではない。ここもうちょっとちゃんと理解したい。

type MyInterface interface{}

func nilOrNotNil() any {
    var p *MyInterface = nil
    return p
}

func main() {
    if nilOrNotNil() != nil {
        fmt.Println("not nil") // reached
    }
}
// main.go:13:5: this comparison is always true (SA4023)

SA5 – Correctness issues

SA5003 - Defers in infinite loops will never execute

deferはその関数がスコープなので、無限ループの中では絶対に実行されない。

SA5011 - Possible nil pointer dereference

ポインタがデリファレンスされる際にnilの可能性があるコードを注意してくれる。

SA6 – Performance issues

SA6005 - Inefficient string comparison with strings.ToLower or strings.ToUpper

strings.EqualFold関数は文字の大文字小文字を区別せずUnicode標準のCase Foldingという操作で比較を行うためstrings.ToLowerやToUpperを行ってから比較するより速い。実際に以下のようなコードで測ってみると速度的にメモリ的にも優れていますね。

var s1 = "HoGe"
var s2 = "hOgE"

func Benchmark_ToLower(b *testing.B) {
    for i := 0; i < b.N; i++ {
        strings.ToLower(s1)
        strings.ToLower(s2)
    }
}

func Benchmark_CaseFolding(b *testing.B) {
    for i := 0; i < b.N; i++ {
        strings.EqualFold(s1, s2)
    }
}
% go test -bench . -benchmem
goos: darwin
goarch: arm64
pkg: sample
Benchmark_ToLower-8             31507820                37.83 ns/op            8 B/op          2 allocs/op
Benchmark_CaseFolding-8         124359349                9.807 ns/op           0 B/op          0 allocs/op
PASS
ok      sample  4.479s

SA9 – Dubious code constructs that have a high probability of being wrong

「間違っている可能性のある」コードを教えてくれる。ただし他のルールに比べてこれは偽陽性の可能性があるとのこと。

SA9005 - Trying to marshal a struct with no public fields nor custom marshaling

非公開のフィールドに対してencoding/jsonなどによるmarshal、unmarshalは効かないのでそれを検知してくれる。これはgo vetでも同様のチェックを行ってくれますよね。

S1 – Code simplifications

S1012 - Replace time.Now().Sub(x) with time.Since(x)

time.Now().Sub(x)よりもtime.Since(x)。このルールに限らず標準パッケージの使い方によってよりシンプルに書ける方法が見つかるので読んでいて楽しい。

S1025 - Don’t use fmt.Sprintf("%s", x) unnecessarily

fmt.Sprintfを使う必要がない場合を検知してくれる。これ結構嬉しいかも、その変数がStringerインタフェースを実装してるのかどうかを毎回完璧に把握するの大変だと思うし。

ST1 – Stylistic issues

ここからデフォルトでは無効化されているルールが登場しますが、見た感じ全て有効化して問題ない気がしました。

ST1000 - Incorrect or missing package comment (non-default)

デフォルトでは無効化されているルール。パッケージコメントをCodeReviewCommentsに記載されているルールで縛るかどうか。

github.com

ST1003 - Poorly chosen identifier (non-default)

デフォルトでは無効化されているルール。変数名やパッケージ名を、Effective GoやCodeReviewCommentsに記載されているルールで縛るかどうか。

これは有効化してもいいんじゃないですかね、mixed-capsとかinitialismsとかが他のリンターで検知できるなら別ですけど。基本この辺りの細かい書き方はEffective GoやCodeReviewCommentsに従って書く人による違いをできるだけなく無くした方が「読みやすい」コードになると思うです。

QF1 – Quickfixes

自動で直されるやつ。

QF1012 - Use fmt.Fprintf(x, ...) instead of x.Write(fmt.Sprintf(...))

こういうのは知らないと気づかないだろうしリンターにチェックしてもらえるなら学習になると思うです。


というわけで全ルールを読んで、その中からかいつまんで紹介しました。知らなかった標準パッケージのAPIを知れたり、Goの仕様を知れたり、Goに限らず「こう書いた方がシンプルだよね」って書き方を再確認したりできました。Go初学者は目を通してみると良いんじゃないでしょうか 👍

『なぜあの人の解決策はいつもうまくいくのか?』を読んだ

www.amazon.co.jp

ラノベみたいなタイトル。システム思考についての本をはじめて読んだのですが、いいですねこれ。

システム思考 is 問題をパターンとして捉えて、パターンを生み出している構造を理解して、構造に働きかけることで問題解決をしていくこと。

私たちは問題があると、すぐに問題解決をしようとし、問題の近くに解決策を探します。この例でいえば「うまくいっていた好循環がうまくいかなくなった。好循環を元通りに回すことが解決であり、そのためにどうしたらよいか」と考えるのです。ところが、解決策は問題の近くにあるとは限りません。一見、問題からはほど遠いところに効果的な解決策がある場合も多いのです。 p17

このあたりは常に気をつけたい。

六人の人が一つのルービックキューブに向かって、それぞれ自分の目の前の一面をきれいに合わせようとしている様子を想像してみてください。自分の目の前を合わせようとキューブを動かすと、その動きは他の人の面に影響を与えます。これではいつまでたっても、六つの面がきれいにそろうことはないでしょう。 p39

全体は部分の総和ではない。

しかし、私たちは「物事を変えたり創り出したりするために必要な時間」を無視して、時期尚早に成果を判断し、せっかくの取組みを「芽が出る前に」摘んでしまうことがよくあります。 p234

シンプルな問題であれば、図8-9のように、まっすぐに望ましい状態に向かって進捗していくでしょう。しかし、複雑で難しい問題の場合は、図8-10のように「いったん悪化してから、改善する」ことがよくあります。「高く飛びあがるには、深く沈み込む必要がある」ともいえるでしょう。 p234

このあたりは時間軸について。

本書で出てくる交通渋滞の例が結構分かりやすいですね。「なぜ道路を拡張したのに渋滞がひどくなったのか?」「なぜ道路を狭くする、自動車の通過に追加で税金を設ける、バス専用車線を追加する、といった直感に反するような解決策がうまくいくのか?」といったところからシステム思考の理解を深めることができます。

構造を理解するためのツールであるループ図は、自分自身で問題を理解したり、チームで共通の理解を促したりするのにかなり役立ちそうだなと思います。問題の近くにある変数だけじゃなく、遠くにある変数も含めてどういうループが回っているのか俯瞰して見れるようになると取りうる解決策の幅がかなり広がるだろうなぁ。

httptest.ResponseRecorderを使ったテストでHTTPステータスコードが意図した値にならないとき

httptest.ResponseRecorderを使ってHTTPサーバーのテストをしている際、レスポンスのHTTPステータスコードが意図した値にならないなぁと思って色々調べたのでそのメモです。

例えば以下のようなコードで再現することができます。

type MyHandler struct{}

func (h *MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("sample"))
    w.WriteHeader(http.StatusTeapot)
}

func main() {
    log.Fatalln(http.ListenAndServe(":8080", &MyHandler{}))
}

このプログラムを実行してリクエストを送るとsampleという文字列がHTTPステータス200で返ってきます。

sample % curl -i localhost:8080
HTTP/1.1 200 OK
~省略~

sample

テストコードを簡単に書くとこのようになりますが、こちらも同様です。

import (
    "net/http"
    "net/http/httptest"
    "testing"
)

func Test(t *testing.T) {
    req := httptest.NewRequest(http.MethodGet, "/", nil)
    res := httptest.NewRecorder()

    h := &MyHandler{}
    h.ServeHTTP(res, req)

    if want, got := http.StatusTeapot, res.Code; want != got {
        t.Errorf("status code should be %d, but got %d", want, got)
    }
}
=== RUN   Test
    main_test.go:18: status code should be 418, but got 200
--- FAIL: Test (0.00s)

FAIL

結論

  • 200以外のHTTPステータスコードを設定したいならWriteメソッドの前にWriteHeaderメソッドを呼び出す必要がある

この辺りは、net/http.ResponseWriterインタフェースのドキュメントに書いてありますね。 pkg.go.dev

// WriteHeader sends an HTTP response header with the provided
// status code.
//
// If WriteHeader is not called explicitly, the first call to Write
// will trigger an implicit WriteHeader(http.StatusOK).
// Thus explicit calls to WriteHeader are mainly used to
// send error codes.
//
// The provided code must be a valid HTTP 1xx-5xx status code.
// Only one header may be written. Go does not currently
// support sending user-defined 1xx informational headers,
// with the exception of 100-continue response header that the
// Server sends automatically when the Request.Body is read.
WriteHeader(statusCode int)

以下、結論を補足する形で実際にhttptest.ResponseRecorderの実装を読んでいきます。

httptest.ResponseRecorderの実装を読んでいく

HTTPサーバーのテストで利用するhttptest.ResponseRecorderの実装を追います。今回は、動機となった「レスポンスのHTTPステータスコードが意図した値にならない」の理由を知ることを目的に読んでいきます。

httptest.ResponseRecorderはhttp.ResponseWriterインタフェースを実装した型なので、Header() Header Write([]byte) (int, error) WriteHeader(statusCode int)の3つのメソッドの周辺を調査します。

まず直接HTTPステータスコードを設定できるWriteHeader()メソッドの実装を眺めます。

func (rw *ResponseRecorder) WriteHeader(code int) {
    if rw.wroteHeader {
        return
    }

    checkWriteHeaderCode(code)
    rw.Code = code
    rw.wroteHeader = true
    if rw.HeaderMap == nil {
        rw.HeaderMap = make(http.Header)
    }
    rw.snapHeader = rw.HeaderMap.Clone()
}

早速レシーバーのwroteHeaderフィールドが真ならリターンしていて怪しいです。wroteHeaderフィールドにはGoDocがありませんでしたが、名前の通りですね。では、いつどこでwroteHeaderフィールドが書き換えられているのか。

wroteHeaderで検索をかけると、参照している箇所がいくつか、値を代入している箇所が1箇所見つかります。値を代入している1箇所がWriteHeaderメソッドの中ですね。

よって同一のResponseRecorderインスタンスでは、WriteHeaderメソッドによるHTTPステータスコードの設定は一度のみできることが分かります。

次にWriteメソッドがどういう実装になっているかを見ます。

func (rw *ResponseRecorder) Write(buf []byte) (int, error) {
    rw.writeHeader(buf, "")
    if rw.Body != nil {
        rw.Body.Write(buf)
    }
    return len(buf), nil
}

1行目でwriteHeaderという非公開メソッドを呼んでいるので、そいつを見にいきます。

func (rw *ResponseRecorder) writeHeader(b []byte, str string) {
    if rw.wroteHeader {
        return
    }
    if len(str) > 512 {
        str = str[:512]
    }

    m := rw.Header()

    _, hasType := m["Content-Type"]
    hasTE := m.Get("Transfer-Encoding") != ""
    if !hasType && !hasTE {
        if b == nil {
            b = []byte(str)
        }
        m.Set("Content-Type", http.DetectContentType(b))
    }

    rw.WriteHeader(200)
}

今回の目的に沿って読むならば、気になる箇所は2箇所あります。まず1行目では、公開メソッドのWriteHeaderと同様にwroteHeaderフィールドがtrueなら何もせずreturnしてますね。そしてメソッドの最後にWriteHeader(200)メソッドを読んでいます。前述した通りWriteHeaderメソッドは内部でwroteHeaderフィールドをtrueにします。よってWriteメソッドは(Bodyへの書き込み前に)HTTPステータスコードに200を設定することがわかります。

ここまで読むと、最初に載せたサンプルコードでなぜHTTPステータスコードが418にならないのか、なぜテストが失敗するのかが理解できます。

func (h *MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("sample")) // この時点で200が設定され
    w.WriteHeader(http.StatusTeapot) // 上書きはできない
}

それから、「同一インスタンスで一度のみ」ということは、例えばTable Driven Testなどでhttptest.ResponseRecorderのインスタンスを使い回しているとHTTPステータスコードが以前の値に固定されてしまうので、サブテスト内で初期化する必要があるということですね。

ドキュメントや実装を読むことで理解を深めることができました。

参考

『ファシリテーションの教科書 - 組織を活性化させるコミュニケーションとリーダーシップ』を読んだ

https://www.amazon.co.jp/dp/B00P28A5M8

『多様性の科学』を読んでからというもの、集団で仕事をする -> 集合知を高める(集団脳を構築する)と考えた時に、集合知を高める観点で周りを見渡すとまた色々な領域で学ぶことがあるな〜と思っています。

会議もそのひとつで、会議は一人ではやらないので、二人以上で集合知を利用してよりよい結論に辿り着くためのイベントなはずです。会議を因数分解していくと「ファシリテーション」があり、そういえばファシリテーションをちゃんと学んだことないな...社会人はみんな新卒1,2年目くらいで学んだりするのかな...ぐぬぬと思ったので本書を手に取りました。

結果からいうと会議のファシリテーションに限らず、人と話すうえで相手の発言をどう整理するかとか、欠落したあるいは曖昧になっている主張や根拠をどう引き出すかなど、日頃の仕事の考え方が変わるような知見に溢れた本でした。やったね。

ただ同時にファシリテーション難しすぎじゃね?とも思いました。考えるべきことが多すぎて、ひとりの人間がやることじゃあないなぁと思ったので、理想は会議の参加者全員が本書に書いてあるような、そもそも会議の構成を意識したり、論点整理力だったり、聴く/伝える力を鍛えたりするのが良いのでしょうね。

『実用Go言語』を読んだ

https://www.amazon.co.jp/dp/4873119693

実務レベルでGoを読み書きする際に考えるであろう様々な「問い」とその回答が体系的にまとまっている超良書でした。ぼくは最近Goを触り始めたペーペーなので、本書を読むと昨日自分が書いたコードを書き直したくなったり、CIの設定を直したくなったり、がいたるところで起こります。

本書が扱う領域が広いので、自分がまだGoでは経験していないテーマ(章単位でいうと9, 12, 15, 16章あたり)も多くありましたが、説明が分かりやすいため一通り読んで頭の中の引き出しに入れておくことが苦ではありませんでした。今後Goを読み書きするうえで、何度も読み返すような本の一冊です。

Goで書かれたコード(同僚が書いたコード、標準パッケージ、準標準パッケージ、サードパーティモジュール)が圧倒的に「読みやすく」なり、書く際の選択肢が理由付きで増えるため「書きやすく」なります。

もうちょい具体的な話でいうと、例えばGoの書き方だけでなくDockerfileのプラクティスにも言及があったり、クラウドストレージやオブザーバビリティの章があったりします。Dockerfileではベースイメージどれ選ぶのがオススメかが書かれていて、その中で「イメージサイズだけ考えてAlpine選ぶと〜」という記述があったりしてこの辺は運用まで含めて実務でGoを使い倒してるからこその現場感のある言及で「実用」みを感じました。

リフレクションと構造体のタグを理解することで、jsonパッケージやcsvパッケージが内部で何をしているのか想像できるようになりますし、HTTPサーバー、HTTPクライアントのところなんかも、標準パッケージをしっかり理解することでgo-chiやechoなどサードパーティモジュールが内部で何をやっているか想像できるようになる感覚は楽しかったです。

読んで良かった〜

『THE CULTURE CODE 最強チームをつくる方法』を読んだ

www.amazon.co.jp

Amazonを見ると去年購入してたらしい。最近チーム関連の本をいくつか読んでたので、その流れでKindleのライブラリにあった本書を読みました。

主題としてはこんな感じ。

チーム力を醸成する文化は「安全な環境」「弱さの開示」「共通の目標」の3つに集約される。 p6

ぼくが本書で印象に残ったメッセージは「弱さを見せる(見せ合う)」と「小さなシグナルを送る」ですね。あぁ、こうやってチームを作るリーダーシップもいいじゃんと思えました。

チームで仕事をする = 集合知によって大きな成果を出すだと最近は思っていて*1、「集合知」という観点と結びつけて読むことですんなり頭に入ってきて読めた感覚があります。

事例もどれも面白くて、特に好きなのはピクサー・ディズニーの話とベル研究所の話かな。あとレッドバルーン・チャレンジの話。挙げたらキリがないです。

今度はリーダーシップについての本もいくつか読もうかなぁ。

*1:最近読んだ『多様性の科学 画一的で凋落する組織、複数の視点で問題を解決する組織』という本が本当に良かったので影響を受けた