はじめに
TIG真野です。育休明けです。
Go言語の特徴の1つに、静的解析ツールがリッチということがあると思いますが、代表格である go vet
と付随する(という表現が正しいか不明ですが)ツール群についてまとめます。知っておくと似たようなツールを作ってしまうことを防げるかなという点と、類似のチェックを行いたい場合に参考にしたいこと、ユースケースが合えばこれらを利用してハッピーになりたいという思いがあります。
go vetとは
go vet はGo言語に標準で組み込まれた静的解析ツールで、コンパイラによってキャッチされないエラーや懸念を検出し報告してくれます。Linter(リンター)の一種です。
チェック内容の一覧は go tool vet help
コマンドで確認できます。デフォルトでは全て有効になっており(-asmdecl=false
などで個別に無効化は可能)、Go1.21時点では30のチェックが存在します。
$ go version |
vetはGo1.19でerrorsasが、Go1.20でloopclosureとtimeformatが追加されるなど、高品質にメンテナンスされているため安心して利用しているチームがほとんどだと思います。さすが標準ツールです。
Go1.20のvetについてはGo 1.20 vetのアップデートの記事もあります。
ちなみに、go test
を実行するとGo 1.10から内部的にgo vetも動作するようになっている そうです。注意として、go testでgo vetのすべてのチェックが動くのではなく、信頼性の高い以下9つののサブセットに限る点です。go test
で go vet
の30種類すべてを動かしたい場合は go test -vet=all
とオプションつけましょう。
- atomic
- bools
- buildtags
- directive
- errorsas
- ifaceassert
- nilfunc
- printf
- stringintconv
go vetを構成する要素
go vet
の実装のうち、 mainパッケージのコードから抜粋します。 unitchecker.Main()
の引数が先程の go tool vet help
結果で出てきたチェック一覧と一致することがわかると思います。
func main() { |
では、引数の asmdecl.Analyzer
~ unusedresult.Analyzer
たちが何かというと、import元のパッケージを見ると、 golang.org/x/tools/go/analysis/passes であることがわかります。 passes
パッケージは golang.org/x/tools/go/analysis
パッケージの静的解析のAPIを利用して作られている集合で、各パッケージごとに静的解析のチェックが実装されているというわけです。わかりやすい構成ですね。
スタンドアローンドライバー
さて、golang.org/x/tools/go/analysis/passes のGoDocを見ると、 foo/cmd/foo といった形式で存在するドライバー(コマンド)が存在します。2023年9月5日公開 Version: v0.13.0だと、以下の5種類です。日本語訳はChatGPTさんにお願いしました。なぜかifaceassert、lostcancel、stringintconv、unmarshal、unusedresultはcmdパッケージがあるものの、go vetに含まれているため除外しています。
これらは先程紹介したgo tool vet help
にも cmd/vet/main.go
にも含まれないため、golang.org/x/tools/go/analysis/passes
パッケージに存在すれど go vet
で行われないチェックツールです。この記事の趣旨はこいつらってどのような存在か抑えておこうというものです。これらの使い方は後述します。
Name | Description |
---|---|
defers | package defersは、defer文の一般的な間違いをチェックするAnalyzerを定義します。 |
fieldalignment | package fieldalignmentは、フィールドがソートされている場合に、より少ないメモリを使用するstructを検出するAnalyzerを定義します。 |
findcall | package findcallは、Analysis APIの単純な例とテストとして機能するAnalyzerを定義します。 |
nilness | package nilnessは、SSA関数の制御フローグラフを検査し、nilポインタの参照や退化したnilポインタの比較などのエラーを報告します。 |
shadow | package shadowは、シャドウされた変数をチェックするAnalyzerを定義します。 |
使い方は5種類すべて同様に以下の流れで利用できます。
- 各 xxx/cmd/xxx をインストールする
go vet
の-vettool
オプションでインストールしたコマンドのパスを渡す-vettool
で渡す値は、絶対パスである必要があるので注意ください(後述でcommand -v
やらを使っている理由がそれです)
まずは5つのツールをすべてインストールします。
go install golang.org/x/tools/go/analysis/passes/defers/cmd/defers@latest |
defers
defersはdefer構文でのよくある間違いを検知してくれるツールです。
検知するためのコードを準備します。このコードはdefersのGoDocに掲載されていた内容なので新規性は無いです。
package example |
# (Windowsでは $()の部分を %USERPROFILE%\go\bin\defer などに書き換えてください) |
-vettool
オプションでdefers
を渡すことで実行できました。最後の行に出ているのが検知したメッセージです。
検知した内容としては vetders.go のL7行目の deferで呼ばれる recordLatency()
の引数、 time.Since(start)
がdeferが呼ばれるタイミングではなく、即時評価されちゃっているということです。
おそらく以下が正しいのでは? という検知です。うっかりやっちゃいそうですね。
defer func() { recordLatency(time.Since(start)) }() |
これが標準で入っていない理由ですが、ハッキリとしたIssueやレビューのやり取りを見つけられずでした。推測ですが、検知するのが time.Since のみなのでピンポイントすぎる内容であること、影響度も処理時間計測で用いられるものが大半だと思われるので、クリティカルで無いとみなされたのかなと思います。
fieldalignment
fieldalignmentはフィールドを並び替えた場合にメモリ消費量が抑えられるstructを検出してくれるツールです。
例えば以下のようなコードがあったとします(※GoDocから引っ張ってきているのでコード例にも新規性は無いです)。
package example |
go vet -vettool=$(command -v fieldalignment) ./... |
実行すると、9行目の Bad という structが検知されます。byte型は1byteです。メモリは4byteごとに確保され(64bit OSだと最大8byteまでは同じ領域に連続して格納できるため)、x,zを連続させると4byteで格納できます。int32(4byte) + byte型の2フィールド(4byte) = 8byteにできるわけです。もし、int32を間に入れると(Bad structの場合)、4byte(x分) + 4byte(y分) + 4byte(z分)で12byteとなってしまい、無駄が生じているよというわけです。
一応、Playgroundも用意しました。気になる方は確認ください。
https://go.dev/play/p/BMtHxH9B_cF
これも go vet
標準に含めな議論を見つけられなかったですが、推測するとStructのフィールドを並び替えることで可読性などを落としメモリ省力化を追求することで利点が多いユースケースがメジャーではないこと(組み込みなど厳しいマッチしたケースは当然あると思います)があるのかなと思います。有用ですがデフォルトで有効にするものではないよねということです。
findcall
findcallは特定の関数・メソッドが呼ばれているかチェックするツールです。 -name
オプションで対象の関数・メソッドを指定します。
package example |
go vet -vettool=$(command -v findcall) -name println ./... |
これについてはユースケースがピンと来ず、CIで組み込むというよりは開発中に個別で気になった関数・メソッドの利用可否をチェックするといった使われ方でしょうか。検査対象の名称を引数で渡す必要があるため、現在の形式では go vet
標準で組み込まれることはない認識です。
静的解析の題材としてtennntenさんが取り上げているイメージがあるので、これを土台として有用なツールが作られており、その観点では重要な位置づけにあるツールに見えます。
https://engineering.mercari.com/blog/entry/2018-12-16-150000/
nilness
nilness は nil ポインターの逆参照などを検知するツールです。例えばif分でnil検査をしているにもかかわらず、その変数にアクセスするとpanicが生じます。そういった実装を検知してくれます。
package example |
>go vet -vettool=$(command -v nilness) ./... |
便利そうな気もしますね。なぜこれが go vet
標準でないのかは良く分かりませんでしたが、プロポーサルに理由が書いてありました。
それによると、go vet
は go test
に最終的にはすべて含まれることを目指しており、 nilness
はそのための(速度)基準をまだ満たしていないといったことが壁のようです。
shadow
shadowはシャドウイングを検知してくれるツールです。シャドウイングについてはmattnさんのGo 言語で変数のシャドウイングを避けたいなら shadow を使おう。記事を参考ください(まさにshadowの紹介もしていますね)。
package example |
これに対してshadowを実行します。
>go vet -vettool=$(command -v shadow) ./... |
12行目の , err := f.Read(buf)
で、 :=
で代入しているため、L10行目で宣言した err
とは同一名称で異なる変数が宣言されています。そのためL14でbreakしても戻り値のerrはnilのままです。いい感じに検知してくれていますね。
シャドウイング自体はGoの言語機能として本来問題ないはずなのでgo vet標準には組み込まれていないのだと思います。
-vettool に複数プログラムを指定できない
なんとなく、以下のように一括でチェックができるイメージがありましたが、以下のような指定はできません。
go vet -vettool=$(command -v defers) \ |
そのため、go vetを複数回起動することになると思います。go vet
標準と、スタンドアローンドライバー4種類を実行する場合は以下のようにコマンドを並べることになると思います。
go vet ./... |
また、 -vettool
を指定すると、go vet組み込み+vettoolのチェックを行うと私は勘違いしてしまいましたが、あくまでvettool
のチェックのみ行われることに注意ください。
まとめ
go vet に近い静的解析ツールを5種類紹介しました。それらが解決する課題がチームにマッチする場合は、有効にして取り入れてると良いかなと思いました。また、類似の課題の場合はそれらのコードを参考に、自分たちで解析ツールを作るといったことにも役立つと思います。
アイキャッチはUnsplashのEtactics Incが撮影した写真です。