コンパイラによってキャッチされない不具合や懸念を検出するためのものとして、静的解析ツールがある。
Goの静的解析ツールはいくつかあるが、ここでは go vetについて紹介する。
go vetとは
Go言語に標準で組み込まれた静的解析ツール。
言語標準ツールのため言語のバージョンアップと同じタイミングで更新される。
公式ドキュメントはこちら。
使用可能なルール
go tool vet help
を実行すると、使用可能なルールの一覧が表示される。Go1.22.3 時点では32のルールが存在する。
$ go tool vet help
...
Registered analyzers:
appends check for missing values after append
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 //go:build and // +build directives
cgocall detect some violations of the cgo pointer passing rules
composites check for unkeyed composite literals
copylocks check for locks erroneously passed by value
defers report common mistakes in defer statements
directive check Go toolchain directives such as //go:debug
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
slog check for invalid structured logging calls
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
timeformat check for calls of (time.Time).Format or time.Parse with 2006-02-01
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
...
appends
ルールは、スライスに追加すべき値を渡していない append 呼び出しを検出する。
s := []string{"a", "b", "c"}
_ = append(s) // append with no values
unmarshal
ルールは引数の型がポインターまたはインターフェースではない json.Unmarshal
などの関数呼び出しを検出する。
import (
"encoding/json"
"io"
)
type Person struct {
Name string
Age int
}
func handler(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Failed to read request body", http.StatusBadRequest)
return
}
var person Person
err = json.Unmarshal(body, person) // call of Unmarshal passes non-pointer as second argument
if err != nil {
http.Error(w, "Failed to unmarshal JSON", http.StatusBadRequest)
return
}
}
各ルールの詳細については、go tool vet help {ルール名}
を実行すると表示される。
$ go tool vet help appends
appends: check for missing values after append
This checker reports calls to append that pass
no values to be appended to the slice.
s := []string{"a", "b", "c"}
_ = append(s)
Such calls are always no-ops and often indicate an
underlying mistake.
go vet の実行方法
go vet ./...
デフォルトでは全てのルールが有効で、特定のルールを実行するには実行オプションとして-{ルール名}
を付与する。
$ go vet -appends ./...
go vet -vettoolで実行する静的解析ツール
go vetのオプション-vettool
を指定して、追加で実行できるルールがある。
各ルールの詳細は「go vet に含まれないスタンドアロンな静的解析ツールたち」で解説されているのでそちらを参照ください。
ここでは nilness, shadow について紹介する。
nilness
例えば if 文で nil 検査をしているにもかかわらず、ポインタ型のフィールドにアクセスしようとするとpanicが生じる。
nillnessはそういった実装を検出してくれる。
type X struct{ f int }
func Fnilness(x *X) {
if x == nil {
fmt.Println(x.f) // nil dereference in field selection
return
}
fmt.Println(x.f)
}
shadow
シャドウイングを検知してくれるツール。
シャドウイングとは、コードブロックの外側にある変数と同名の変数をコードブロックの内側で宣言した時、内側からは内側の変数にアクセスするというもの(外側の変数にアクセスできない)。
以下のコードでは 変数名err
が シャドウイングに当たり、f.Read()
でエラーが発生してもfor文の外側で宣言された err
には格納されないため、BadRead()
は常に nil を返すことになる。
func BadRead(f *os.File, buf []byte) error {
var err error
for {
_, err := f.Read(buf) // declaration of "err" shadows declaration
if err != nil {
break
}
}
return err
}
これを改善するにはfor文の内側で err
が再定義されないように _, err = f.Read(buf)
とする。
vettoolの実行方法
各vettoolをインストールする。
go install golang.org/x/tools/go/analysis/passes/nilness/cmd/nilness@latest
go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest
go vet の -vettool
には複数のルールを指定できないため、個別に実行してあげる必要がある。
go vet ./...
go vet -vettool=$(which nilness) ./...
go vet -vettool=$(which shadow) ./...
指摘を無視したい時
go vet
による指摘は基本的には直すのが良い。
しかし、ライブラリや自動生成ファイルなどの自分たちで管理していないコードは静的解析対象外にしたいケースもある。
例えば、次のようなディレクトリ構成で mock
ディレクトリには mockgen を使って自動生成したファイルを格納しており対象外にしたいとする。
├── go
├── app
├── domain
├── go.mod
├── go.sum
├── infra
├── lib
├── main.go
├── mock // 静的解析対象外にしたい
└── presentation
この時、次のコマンドを実行すると mock
ディレクトリ配下のコード以外を静的解析の対象にできる。
go vet $(go list ./... | grep -v '{module-name}/mock')
{module-name}
はgo.modに書かれた module名で、grep -v '{module-name}/mock'
によって mockディレクトリ配下にあるパッケージを除いた全パッケージを指定している。