👻

Staticcheck: go vetコマンドを補完するGo向けリンタの導入

2024/02/14に公開

アプリケーションプロジェクトの開発では、パッケージ管理、テストコード、リンタ、フォーマッタの実行環境さえ用意できていれば、必要最低限それっぽくなるとこれまでの記事で繰り返し主張してきた。

Goについては使い始めて間もないが、他の言語と比べたGoの良い点として、これらのツールがすべて公式から提供されている点が挙げられる。

パッケージ管理はgo mod、テストコードはgo test、リンタはgo vet、フォーマッタはgo fmtコマンドといったように、サードパーティのパッケージをインストールしなくてもプロジェクト開発の準備が整う。

なお自分がこれらツール群を調べた際に、リンタについてはGo公式のlintが廃止されたといった記事を最初に見つけたことから、go vetの存在を意識せずに代替ツールをいくつか調査したところ、Staticcheckがシェアの獲得度、メンテナンスの継続性、導入&利用の容易さの点から良さげだったので、導入作業を開始した。

が、のちにgo vetコマンドの存在に気づき、余計なツールを増やすくらいならStaticcheckを使わない方がいいのではとも考えたが、go vetコマンドがチェックしてくれる項目が現時点で32なのに対して、Staticcheckha150以上の項目のチェックを行なってくれるとのこと。

別に数が多ければ良いというものでもないが、個人的に欲しい"変数や関数のunusedチェック"がStaticcheckにのみ含まれていたりするのと、基本的には設定不要でstaticcheck ./...コマンドを実行するだけで利用可能といった学習/導入コストの安さから、結局は採用することにした。

以下では、Staticcheckの導入と利用方法についてまとめてみた。

インストール

Staticcheckの導入は以下のコマンドを発行するだけでよい。

$ go install honnef.co/go/tools/cmd/staticcheck@latest

その他のインストール方法はこちら

なお、Staticcheckをインストールする前に、Go自体をまずインストールする必要があるため、必要に応じてgoenvなどで事前にインストールしておく必要がある。

ちなみにいつもならMacのhomebrewでインストールをおすすめするところだが、homebrewでインストールすると不要なGoを勝手にダウンロード&インストールする上に、goenvで使用しているバージョンを無視して動かなくなるという問題が発生するので今回はhomebrewを使わない。

あと、CIで上記インストールコマンドを発行する場合、新バージョン登場による意図しない挙動の変更を回避するためにlatestではなく固定バージョンを記述する。

(CIでの利用の別選択肢として、公式がGithub ActionsのActionを紹介しているので、興味があればそちらも確認してほしい)

検証

ここからは、Staticcheckを実際のGoプロジェクトで利用する手順を説明していく。

まず、新しいツールを使い始めるにあたって、手始めとしてツールの設定方法のドキュメントを先に読んでおきたい。

自分が読んだ感想としては、設定は一切不要でデフォルトのまま利用を開始して問題ないと感じた。

たいていリンタを使う場合、全チェック項目を確認してから自分の開発ポリシーに応じて調整する必要があったりするが、デフォルトの設定の細かさやドキュメントの端々から感じられる開発者の自信から、とりあえずデフォルトのまま使ってみる。

もし、実装の障害となるルールが現れたら、設定ファイル(staticcheck.conf)を作成して、checksの値を都度微調整するといったスタンスで運用していく。

それでは、go modコマンドを使用してサンプルプロジェクトを作成する。

$ mkdir /tmp/sample-project
$ cd /tmp/sample-project
$ go mod init sample
go: creating new go.mod: module sample
$ cat go.mod
module sample

go 1.22.0

ちなみに、Staticcheckが提案するコードは、go.modのgoディレクティブに書かれているバージョンを参考にする。

たとえばGoの新しいバージョンでしか使えない良い記法があったとして、go.modに記述されているバージョンがそのバージョン未満だった場合は、Staticcheckはその提案を控えるようになっている。

話が逸れたが、次にサンプルコードを作成する。

/tmp/sample-project/hello_world.go
$ vi /tmp/sample-project/hello_world.go
package main

func hello() string {
  return "Hello"
}

func world() string {
  return "World"
}

それからサンプルコードに対するテストコードを作成。

/tmp/sample-project/hello_world_test.go
$ vi /tmp/sample-project/hello_world_test.go
package main

import (
  "testing"

  "github.com/stretchr/testify/assert"
)

func TestHello(t *testing.T) {
  assert.Equal(t, "Hello", hello())
}

上記テストコードではtestify/assertパッケージを利用しているので、以下のコマンドでダウンロードする必要がある。

(他の言語と違って、ソースコードにパッケージの利用を宣言してからダウンロード出来るのが珍しい)

$ go mod tidy
$ cat go.mod
module sample

go 1.22.0

require github.com/stretchr/testify v1.8.4

require (
	github.com/davecgh/go-spew v1.1.1 // indirect
	github.com/pmezard/go-difflib v1.0.0 // indirect
	gopkg.in/yaml.v3 v3.0.1 // indirect
)

これでテストの準備が整ったのでテストコードを実行する。

# "./..." はカレントだけでなくサブディレクトリ内まで再帰的にgo testコマンドを適用するという意味
$ go test ./...
ok  	sample	0.340s

テストも無事パスしたため、次はリンタを実行する。

まずgo vetコマンドを実行してみると、特にコードに問題なしと判定される。

$ go vet ./...
$ echo $?
0

次にstaticcheckコマンドを実行すると、world関数が未使用のため以下のようなエラーが発生する。

$ staticcheck ./...
hello_world.go:7:6: func world is unused (U1000)
$ echo $?
1

この結果を受けて、world関数を消せばチェックをパスするようになるが、後で使う予定がある場合は以下のようなコメント(公式ではlinter directiveと呼んでいる)をworld関数に書けばいい。

/tmp/sample-project/hello_world.go
$ vi /tmp/sample-project/hello_world.go
package main

func hello() string {
  return "Hello"
}

//lint:ignore U1000 (ignore理由をここに書く->) あとで使う。
func world() string {
  return "World"
}

$ staticcheck ./...
$ echo $?
0

なお// lint:ignoreのようにスペースを空けるとうまく認識しない点に注意。

Staticcheckが発したエラーの理由を詳しく知りたい場合は、ブラウザ経由でいちいちエラーメッセージを検索をしなくても、staticcheck -explain [code]コマンドで簡単な説明とオンラインドキュメントへのリンクを確認出来る。

(U1000の原因は自明だから大したこと書いていない&オンラインドキュメント無い?)

$ staticcheck -explain U1000
Unused code

Available since
    unreleased

Online documentation
    https://staticcheck.io/docs/checks#U1000

あとコードファイル全体にルールの無効化を適用したい場合はコードの1行目、つまりpackage mainの上に//lint:file-ignore U1000 (ignore理由をここに書く)と書けばいい。

最後にフォーマッタの実行も忘れずに。

$ go fmt ./...
hello_world.go
hello_world_test.go

おわり

以上でStaticcheckの導入と利用方法を説明した。

ついでに、Goはデフォルトのツールだけでも十分に開発を始められることも伝わったと思う。

Discussion