こんにちは、バックエンドエンジニアをしています武山 (bushiyama) です。
ANDPADボードという稼働管理アプリのバックエンドを担当しています。
これはなに
Go 1.22 リリースパーティにて登壇発表した内容を、実例コードを添えてあらためて起稿したものです。
当日の資料は別途こちらを参照ください。
発表したこと
Go 1.22 のお祝いということでリリースノートのTool関連から、Vet周りの更新内容について発表してきました。
おさらい
まずおさらいからということで、Vetとはなにか。
上記ドキュメント Overview にある通り、以下のような意訳となります。
Go のソースコードをコードを検査し、不審な構造を報告します。
すべてのレポートが真の問題であることを保証するものではありませんが、コンパイラーによって捕捉されなかったエラーを見つけることはできます。
弊チームの Vet ユースケース
VSCode勢には、初期設定で自動実行してくれる身近な存在ですが、弊チームでは CI でもあらためて golangci-lint から実行しています。
... linters: enable: ... - govet ...
ブラウザからの杜撰なコミットを防止してくれたりなど、活躍しています。
ちなみに、golangci-lint での 1.22 対応は、v1.56.1 で対応済みのようです。
前 version からのリリースノート比較
v1.21.0 からの変更点のダイジェストがこちらです。
v1.22.0 リリースノートに記載のあったものはもちろん、なかったものも全て紹介していきます。
omit
References to loop variablesnew
appends check for missing values after appendupdate
buildtag
check //go:build and // +build directives ※リリースノート記載なし。new
defers
report common mistakes in defer statementsdefaultJoin
directive
check Go toolchain directives such as //go:debug ※リリースノート記載なし。defaultJoin
errorsas
report passing non-pointer or non-error values to errors.As ※リリースノート記載なし。defaultJoin
framepointer
report assembly that clobbers the frame pointer before saving it ※リリースノート記載なし。defaultJoin
ifaceassert
detect impossible interface-to-interface type assertions ※リリースノート記載なし。defaultJoin
sigchanyzer
check for unbuffered channel of os.Signal ※リリースノート記載なし。update
slog
check for invalid structured logging callsdefaultJoin
stringintconv
check for string(int) conversions ※リリースノート記載なし。defaultJoin
testinggoroutine
report calls to (*testing.T).Fatal from goroutines started by a test ※リリースノート記載なし。defaultJoin
timeformat
check for calls of (time.Time).Format or time.Parse with 2006-02-01 ※リリースノート記載なし。
ループ変数への参照(References to loop variables)
🎉ループ変数への参照問題が解消されたことにより、警告されることがひとつ減りました。
Go 1.20 でVetに追加された、以下の警告が役目を終えます。
loop variable i captured by func literal
他の方の発表*1で詳しく触れられましたが、もうループ内で i := i
を書かなくてよくなります。
逆に v1.22 で以下のような、旧来のコード記述でも問題のないことを確認しています。
for i := 0; i < 3; i++ { i := i wg.Add(1)
go.mod とのversion乖離に注意
ちなみに go.mod は 1.21 以前のままでも v1.22 で run/build した際には v1.22 の挙動になります。
local だけ v1.22 にしている場合などは挙動に差分がでるので注意が必要そうです。
必ず go.mod を確認しましょう。
以下 Playground で version を切り替えて試せます。
https://go.dev/play/p/OBdtXOrFkQU
※ go.dev ドメインに移行してから2年ほど vet が効かないようでしたが、最近しれっと復活していました🎉
append (new)
New warnings for missing values after append
🎉append()は引数が無くても問題ありませんでしたが、今回Vetで警告とするようになりました。
appends check for missing values after append
append with no values
staticcheckでは2019年から実装されていたものと同じ内容ですね。
https://staticcheck.io/docs/checks/#SA4021
SA4021 - x = append(y) is equivalent to x = y
append()で引数がなくて問題ないのは、 Go の組み込み関数で扱いが特別なためのようです。
https://go.dev/play/p/iTO8MnZx9Hg
buildtag (update)
既存のbuildtagで変更があったようです。
v1.21.0 doc check that +build tags are well-formed and correctly located
v1.22.0 doc check //go:build and // +build directives
更新が多く差分は調べきれませんでしたが、これはそもそもなにをするものなのかをご紹介。
コード内で使用されていないbuildtagや矛盾するtagなどを検出します。
以下のようなコードが対象です。
新しく入りそうなもの
関連で調べていたところ、以下のようなスペース混入を検知する実装が新たに入りそうでした。
これは助かりそうです。
// go:embed <= NG //go:embed <= OK
defers (new)
New warnings for deferring time.Since
🎉defer で time.Since() をしてしまっているものを警告するようになりました。
defers report common mistakes in defer statements
call to time.Since is not deferred
以下は一見、最後にstart からdeferで関数が終了した経過時間を出してくれそうに思えますが、コード記述された出力結果を保持して最後にfmt.Printするといった挙動になります。
start := time.Now() defer fmt.Println(time.Since(start)) time.Sleep(1 * time.Second) // => 0s
defer の引数とした関数は即時に評価されるためです。
しかし Since() の省略形でない、 time.Now().Sub(t)
とした場合は警告されません。
start := time.Now() defer fmt.Println(time.Now().Sub(start)) time.Sleep(1 * time.Second) // => 同様に 0s
この time.Now().Sub(t)
とする記述については別途、 staticcheck を行うと警告されます。
S1012 - Replace time.Now().Sub(x) with time.Since(x)
タライ回しの感がありますが、これに従い time.Since() を利用するようにすると今度は本題の警告がされます。
defers という名称だと他にも色々と見てくれそうに思えますが、あくまで time.Since() の利用だけしか見ません。少し残念です。
https://go.dev/play/p/tYVzgWNL4Vl
directive (new)
check Go toolchain directives such as //go:debug
昔から存在はしていましたが、 default 実行リストに仲間入り。
Go本体外での Go ツールチェーンで debug などの指定を警告します。
https://go.dev/play/p/Ozzv071E0XW
errorsas (new)
report passing non-pointer or non-error values to errors.As
昔から存在はしていましたが、 default 実行リストに仲間入り。
errors.As にポインタやエラーでない値を渡すことを警告してくれます。
https://go.dev/play/p/XqKfOHGbgXH
import "errors"
のみサポートとしているようなので注意が必要です。
framepointer (new)
report assembly that clobbers the frame pointer before saving it
昔から存在はしていましたが、 default 実行リストに仲間入り。
スタックトレース周りの警告になります。
以下のようなコードが対象のようです。
ifaceassert (new)
detect impossible interface-to-interface type assertions
昔から存在はしていましたが、 default 実行リストに仲間入り。
インターフェイス間の不可能なアサーションを検知します。
https://go.dev/play/p/I2WzPnMm7oW?v=goprev
いままで default じゃなかったんですねえ。
sigchanyzer (new)
check for unbuffered channel of os.Signal
昔から存在はしていましたが、 default 実行リストに仲間入り。
signal.Notify() に渡す chan の容量が確保(バッファリング)されていないと警告されます。
https://go.dev/play/p/8cHWoe5mZJw
いままで default じゃなかったんですねえ。
mainに書いて碌にtestもできずにそのままというパターンが結構ありそうなので助かりそうです。
slog (update)
New warnings for mismatched key-value pairs in log/slog calls
🎉log/slogで、引数のkeyが文字列でない場合や数が一致しない場合、警告するようになりました。
slog.Info arg "hoge" should be a string or a slog.Attr (possible missing key or value)
以下のような場合でBADKEY表記になってしまうパターンですね。
log.Info("message", "key1", "value1", 2, 3, 4)
https://go.dev/play/p/UqdHB4eiobx
こちらは、 Go 1.21 にも出荷済みの内容のようです。
stringintconv (new)
check for string(int) conversions
昔から存在はしていましたが、 default 実行リストに仲間入り。
整数から文字列への型変換を警告します。
https://go.dev/play/p/5jr-o423ZD5
string(x) -> string(rune(x))
とするか、または多くの場合 strconv.Itoa(i)
をすべきかと思われます。
testinggoroutine (new)
report calls to (testing.T).Fatal from goroutines started by a test
昔から存在はしていましたが、 default 実行リストに仲間入り。
test でから呼ばれている goroutine で t.Fatal() しているという警告ですね。
https://go.dev/play/p/4YJCj0pfW94
残念ながら httptest などwrap されている goroutine の検知はできないようでした。
これに関しては、tenntennさんが以前、起稿されています。
httptest のdocへ t.Fatal しちゃダメ等を記載してほしいとか、Vetの要件を満たせないなら golangci-lint にほしいとか、弊社のgopher会で少し盛り上がりました。
timeformat (new)
check for calls of (time.Time).Format or time.Parse with 2006-02-01
昔から存在はしていましたが、 default 実行リストに仲間入り。
2006-02-02
や2006-01-01
は検出してくれないのが微妙な感あります。
https://go.dev/play/p/TgTg5Wh_74y
default とするほどの内容か疑問でしたが、Vetに入るということは一定数以上の対象が存在しているということですね。
rsc 氏も驚いていました。
いやでもうちは大丈夫だよと思っていたら、弊社privateのとあるrepoで time.ParseInLocation()
にて2006-02-01
していたのを発見。
time.ParseInLocation()
は本件の対象外ですが、検知するキッカケになってよかったです。
さいごに
Vetとうまく付き合って、不幸なバグを回避していきたいですね。
アンドパッドでは Go の変更点をつつきながら、キャッキャウフフと楽しめる Gopher を募集しています。