Google Colabで学ぶgofmt入門では、gofmtが「見た目」だけを整えるツールであることを確認しました。
go vetがやってくれることは、「コンパイルが通るか」 と 「go vetが通るか」 を、2軸のマトリクスとして整理するのがわかりやすいです。
go vetが通る |
go vetが通らない |
|
|---|---|---|
| コンパイルが通る | ① 何も問題なし(のはず) | ② go vetの本領発揮ゾーン |
| コンパイルが通らない | ④ 存在しない組み合わせ | ③ コンパイラ自身のエラーがそのまま出る |
本記事では、この4つのマス目を実際にコードで埋めながら、go vetが担っている役割の輪郭をはっきりさせていきます。
すぐに使えるチートシートはこちら Google Colab版
すぐに実行して、試せるコードレシピはこちら。
前提
Google Colabで学ぶgofmt入門を先に読んでおくと、gofmtとの対比がわかりやすいです。とはいえgo vet自体はシンプルな道具なので、前提知識がなくても十分についていけると思われます。
0. 準備 ── go vetは追加インストール不要
go vetもgofmtと同じく、Go本体に同梱されているサブコマンドです。goコマンドが使える環境であれば、それだけで使えます。
!apt-get install -y golang-go
!go version
golang-go is already the newest version (2:1.18~0ubuntu2).
0 upgraded, 0 newly installed, 0 to remove and 3 not upgraded.
go version go1.18.1 linux/amd64
② コンパイルは通るが、go vetは通らないゾーン ── ここが本題
go vetが本当に価値を発揮するのは、まさにこのマス目です。「文法的には正しいのでgo buildは通るが、実行すると意図と違う動きをする」コードを、3パターン試します。
検証2-1: Printfの書式指定と引数の型が食い違っている
%d(整数のフォーマット指定子)に、文字列を渡してみます。
package main
import "fmt"
func main() {
name := "world"
fmt.Printf("%d\n", name)
}
!go build vet1.go && echo "OK: コンパイル成功"
OK: コンパイル成功
go buildはすんなり通ります。文法としては正しいからです。Printfは可変長引数を取る普通の関数にすぎず、コンパイラは「%dに文字列を渡してはいけない」というルールまでは知りません。
!go vet vet1.go; echo "vet exit: $?"
# command-line-arguments
./vet1.go:7:2: fmt.Printf format %d has arg name of wrong type string
vet exit: 2
コンパイルは通る(①のマスに入りそうに見える)のに、go vetにかけると①ではなく②のマスだったことが判明します。Printfは可変長引数を取るただの関数なので、コンパイラは「%dに文字列を渡してはいけない」というルールまでは知りません。printfアナライザがそこを補っています。
検証2-2: 到達不能コード(unreachable code)
すべての分岐でreturnした後に、消し忘れたコードが残っているケースです。
package main
import "fmt"
func f(x int) string {
if x > 0 {
return "positive"
} else {
return "non-positive"
}
fmt.Println("ここには絶対に来ない")
return ""
}
func main() {
fmt.Println(f(5))
}
!go build vet2.go && echo "OK: コンパイル成功"
OK: コンパイル成功
!go vet vet2.go; echo "vet exit: $?"
# command-line-arguments
./vet2.go:11:2: unreachable code
vet exit: 2
ifとelseの両方がreturnしているので、11行目以降は絶対に実行されません。しかし文法的には矛盾がないためgo buildは通ります。ここでも「コンパイル○・vet✕」という②のパターンです。
検証2-3: sync.Mutexを値渡ししてしまうミス
package main
import (
"fmt"
"sync"
)
type Counter struct {
mu sync.Mutex
n int
}
func increment(c Counter) {
c.mu.Lock()
defer c.mu.Unlock()
c.n++
}
func main() {
c := Counter{}
increment(c)
fmt.Println(c.n)
}
!go build vet4.go && echo "OK: コンパイル成功"
OK: コンパイル成功
!go vet vet4.go; echo "vet exit: $?"
# command-line-arguments
./vet4.go:13:18: increment passes lock by value: command-line-arguments.Counter contains sync.Mutex
./vet4.go:21:12: call of increment copies lock value: command-line-arguments.Counter contains sync.Mutex
vet exit: 2
sync.Mutexを含む構造体を値渡しすると、ロックがコピーされて壊れます。コンパイラはコピー自体を禁止していませんが、copylocksアナライザが、定義側・呼び出し側の両方でこれを検出します。ここまでの3つが、go vetが最も存在感を発揮する②のゾーンです。
③ コンパイルが通らないゾーン ── go vetはどう振る舞うか
次に、そもそもコンパイルが通らないコードにgo vetをかけると何が起きるかを確認します。
検証3-1: 未使用変数(コンパイラが検出する意味エラー)
package main
import "fmt"
func main() {
x := 1
y := 2
fmt.Println(x)
}
!go build case_unused.go
# command-line-arguments
./case_unused.go:7:2: y declared but not used
!go vet case_unused.go; echo "vet exit: $?"
# command-line-arguments
vet: ./case_unused.go:7:2: y declared but not used
vet exit: 2
yを宣言だけして使っていないので、go build自体が失敗します。ここでgo vetをかけても、vet:というプレフィックスが付いただけの、ほぼ同じエラーメッセージが返ってきました。go vet独自の指摘(printfやcopylocksなど)は一切出ていません。
検証3-2: 構文エラー(閉じ括弧の消し忘れ)
package main
import "fmt"
func main() {
fmt.Println("hello")
!go build case_syntax.go
# command-line-arguments
./case_syntax.go:7:1: syntax error: unexpected EOF, expecting }
!go vet case_syntax.go; echo "vet exit: $?"
# command-line-arguments
vet: ./case_syntax.go:6:23: expected '}', found 'EOF'
vet exit: 2
ここでもgo vetは独自の分析を行わず、コンパイラが検出するのと同じ構文エラーをvet:付きでそのまま返しているだけです。
つまりgo vetは、コードが正常にコンパイル(型チェックまで含めて)できることを大前提とした、いわば「その先」のチェックです。 コンパイルが通らない時点で、go vet独自のアナライザ(printf・unreachable・copylocksなど)は一つも実行されません。
④ 「コンパイルが通らないのにgo vetは通る」は存在するか
ここまでの検証3-1・3-2からわかるとおり、go vetは内部的にコードを型チェックしてからでないと、独自のアナライザを走らせられません。したがって、
- コンパイルが通らない ⇒
go vetも必ず失敗する(同じエラーがそのまま出るか、go vet自身がそこで止まる)
という一方向の関係になっており、「コンパイルは通らないのにgo vetだけは通る」という④のマスは、理屈の上でも実際の挙動としても存在しません。 これはgofmtとの大きな違いでもあります。gofmtは構文木さえ解析できれば整形できるので、型チェック(意味解析)までは踏み込みません。一方go vetは、型チェックが通った後の「意味」を扱う道具なので、型チェック自体が通らなければ土俵に上がれない、という構造的な違いがあります。
隠れた注意点: 「①コンパイル○・vet○」でも、実はバグっているケース
最後に、一番見落としやすいパターンを確認します。①のマス(コンパイルもgo vetも通る)に入っているのに、実際にはバグっているコードです。
検証5-1: 構造体タグのキー名タイポ
package main
import (
"encoding/json"
"fmt"
)
type User struct {
Name string `jsonn:"name"`
Founded int `json:"founded"`
}
func main() {
u := User{Name: "InclusiveSolutions", Founded: 2024}
b, _ := json.Marshal(u)
fmt.Println(string(b))
}
!go build vet3.go && echo "OK: コンパイル成功"
!go run vet3.go
OK: コンパイル成功
{"Name":"InclusiveSolutions","founded":2024}
jsonnとタイポしているせいで、Nameフィールドのタグが無視され、大文字始まりの"Name"のまま出力されています(Founded側は正しく"founded")。コンパイルもgo vetも通ってしまう、①のマスに紛れ込んだバグです。
!go vet vet3.go; echo "vet exit: $?"
vet exit: 0
go vetにはstructtagというアナライザがありますが、これは「タグの構文が壊れていないか」だけをチェックする仕組みで、キー名の綴りが正しいかどうかまでは検証しません。念のため、本当に構文が壊れているケースと比較します。
package main
import "fmt"
type Item struct {
Name string `json:"name" `
Price int `json:price`
}
func main() {
fmt.Println(Item{})
}
!go vet vet3b.go; echo "vet exit: $?"
# command-line-arguments
./vet3b.go:7:2: struct field tag `json:price` not compatible with reflect.StructTag.Get: bad syntax for struct tag value
vet exit: 2
json:priceのようにダブルクォートで囲まれていない、構文として壊れているタグは②のマスに移動して検出されます。しかしjsonn:"name"のように構文としては正しいがキー名が間違っているケースは、①のマスに留まったまま気づけません。
検証5-2: 変数のシャドーイング
package main
import (
"errors"
"fmt"
)
func doSomething() error {
return errors.New("boom")
}
func main() {
x := 1
if true {
x := 2
fmt.Println("内側のx:", x)
}
fmt.Println("外側のx:", x)
err := doSomething()
if err != nil {
if err := doSomething(); err != nil {
fmt.Println("内側のerr:", err)
}
}
fmt.Println("外側のerr:", err)
}
!go build vet5.go && echo "OK: コンパイル成功"
!go vet vet5.go; echo "vet exit: $?"
OK: コンパイル成功
vet exit: 0
内側のifブロックでx := 2、err := doSomething()と、:=で外側と同名の変数を新しく宣言してしまっています(「代入」のつもりでも、実際は別の変数)。これも①のマスに紛れ込んだままです。
念のため、標準で登録されているアナライザの一覧を確認します。
!go tool vet help
vet is a tool for static analysis of Go programs.
vet examines Go source code and reports suspicious constructs,
such as Printf calls whose arguments do not align with the format
string. It uses heuristics that do not guarantee all reports are
genuine problems, but it can find errors not caught by the compilers.
Registered analyzers:
asmdecl report mismatches between assembly files and Go declarations
assign check for useless assignments
atomic check for common mistakes using the sync/atomic package
bools check for common mistakes involving boolean operators
buildtag check that +build tags are well-formed and correctly located
cgocall detect some violations of the cgo pointer passing rules
composites check for unkeyed composite literals
copylocks check for locks erroneously passed by value
errorsas report passing non-pointer or non-error values to errors.As
framepointer report assembly that clobbers the frame pointer before saving it
httpresponse check for mistakes using HTTP responses
ifaceassert detect impossible interface-to-interface type assertions
loopclosure check references to loop variables from within nested functions
lostcancel check cancel func returned by context.WithCancel is called
nilfunc check for useless comparisons between functions and nil
printf check consistency of Printf format strings and arguments
shift check for shifts that equal or exceed the width of the integer
sigchanyzer check for unbuffered channel of os.Signal
stdmethods check signature of methods of well-known interfaces
stringintconv check for string(int) conversions
structtag check that struct field tags conform to reflect.StructTag.Get
testinggoroutine report calls to (*testing.T).Fatal from goroutines started by a test.
tests check for common mistaken usages of tests and examples
unmarshal report passing non-pointer or non-interface values to unmarshal
unreachable check for unreachable code
unsafeptr check for invalid conversions of uintptr to unsafe.Pointer
unusedresult check for unused results of calls to some functions
By default all analyzers are run.
この一覧のとおり、shadow(シャドーイング検出)は標準の登録済みアナライザに含まれていません。 シャドーイング検出はgolang.org/x/tools配下の別ツールとして提供されており、go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latestのように別途インストールし、go vet -vettool=$(which shadow) ./...のような形で明示的に組み込む必要があります。標準のgo vetだけでは、①のマスに紛れ込んだこの手のバグは検出できません。
マトリクスのまとめ
検証結果を、最初に出した2x2表に実際の事例で埋め直します。
go vetが通る |
go vetが通らない |
|
|---|---|---|
| コンパイルが通る | 正常なコード / ただしjsonnタグのタイポやシャドーイングのように、実は隠れたバグがあっても①に留まる場合がある |
Printf型不一致・到達不能コード・sync.Mutexの値渡しなど、go vetが本領を発揮するゾーン |
| コンパイルが通らない | 存在しない(理屈上ありえない) | 未使用変数・構文エラーなど。go vetは独自の分析をせず、コンパイラと同じエラーをvet:付きでそのまま返す |
go vetは「コンパイラの型チェックが通った、その先」を扱う道具であり、コンパイルが通らない場合はそもそも土俵に立てません。そして「コンパイルもgo vetも通る」からといって、バグが無いとは限らない、という点が今回の検証で一番実感できた部分です。
弊社について
本記事を書いている 合同会社インクルーシブソリューションズ は、データ基盤構築・分析基盤設計・システム改善支援を中心に活動している小規模IT法人です。
主な領域は、
- データマート設計・データパイプライン構築
- SQL / Python を用いたデータ処理設計
- BI導入支援・分析基盤の整備
- 既存システムの運用改善・可視化支援
といった、「データを使える状態にする」ための活動です。
弊社の企業活動に興味がある方は、ぜひ公式サイトも覗いてみてください。