Go 1.22 の Vet 変更点について

こんにちは、バックエンドエンジニアをしています武山 (bushiyama) です。

ANDPADボードという稼働管理アプリのバックエンドを担当しています。

andpad.jp

これはなに

Go 1.22 リリースパーティにて登壇発表した内容を、実例コードを添えてあらためて起稿したものです。

当日の資料は別途こちらを参照ください。

speakerdeck.com

発表したこと

Go 1.22 のお祝いということでリリースノートのTool関連から、Vet周りの更新内容について発表してきました。

おさらい

まずおさらいからということで、Vetとはなにか。

pkg.go.dev

上記ドキュメント Overview にある通り、以下のような意訳となります。

Go のソースコードをコードを検査し、不審な構造を報告します。
すべてのレポートが真の問題であることを保証するものではありませんが、コンパイラーによって捕捉されなかったエラーを見つけることはできます。

弊チームの Vet ユースケース

VSCode勢には、初期設定で自動実行してくれる身近な存在ですが、弊チームでは CI でもあらためて golangci-lint から実行しています。

...
linters:
  enable:
  ...
  - govet
  ...

ブラウザからの杜撰なコミットを防止してくれたりなど、活躍しています。

ちなみに、golangci-lint での 1.22 対応は、v1.56.1 で対応済みのようです。

github.com

前 version からのリリースノート比較

v1.21.0 からの変更点のダイジェストがこちらです。

go.dev

v1.22.0 リリースノートに記載のあったものはもちろん、なかったものも全て紹介していきます。

  • omit References to loop variables
  • new appends check for missing values after append
  • update buildtag
    check //go:build and // +build directives ※リリースノート記載なし。
  • new defers
    report common mistakes in defer statements
  • defaultJoin 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 calls
  • defaultJoin 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などを検出します。

以下のようなコードが対象です。

https://cs.opensource.google/go/x/tools/+/refs/tags/v0.19.0:go/analysis/passes/buildtag/testdata/src/a/buildtag.go

*2

新しく入りそうなもの

関連で調べていたところ、以下のようなスペース混入を検知する実装が新たに入りそうでした。

これは助かりそうです。

// go:embed <= NG
//go:embed <= OK

github.com

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 実行リストに仲間入り。

スタックトレース周りの警告になります。

以下のようなコードが対象のようです。

https://cs.opensource.google/go/x/tools/+/refs/tags/v0.19.0:go/analysis/passes/framepointer/testdata/src/a/

*3

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さんが以前、起稿されています。

zenn.dev

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-022006-01-01は検出してくれないのが微妙な感あります。

https://go.dev/play/p/TgTg5Wh_74y

default とするほどの内容か疑問でしたが、Vetに入るということは一定数以上の対象が存在しているということですね。

rsc 氏も驚いていました。

github.com

いやでもうちは大丈夫だよと思っていたら、弊社privateのとあるrepoで time.ParseInLocation() にて2006-02-01していたのを発見。

time.ParseInLocation() は本件の対象外ですが、検知するキッカケになってよかったです。

さいごに

Vetとうまく付き合って、不幸なバグを回避していきたいですね。

アンドパッドでは Go の変更点をつつきながら、キャッキャウフフと楽しめる Gopher を募集しています。

engineer.andpad.co.jp

*1:悪名高いループ変数が共有される問題が解決した話 by ryotaku nakagawa

*2:Playgroundで試せず。困った時の公式test参照

*3:Playgroundで試せず。困った時の公式test参照