Go 1.4 が出て go generate コマンドが登場しました。 go generate コマンドの代表的な利用例として紹介されているのが stringer です。 新しもの好きな人は定数にどんどん stringer をかけていってることでしょう。
そんな stringer ですが、次のようなケースでちょっとハマりました。
package pkg
import "pkg/sub"
//go:generate stringer -type=aaa
type aaa byte
const (
a aaa = iota
b
c
d
)
func Xyzzy(a aaa, b sub.bbb) {
}
package sub
func Hello() {}
$ GOPATH=`pwd` go generate pkg
stringer: checking package: aaa.go:3:8: could not import pkg/sub (can't find import: pkg/sub)
src/pkg/aaa.go:5: running "stringer": exit status 1
原因
stringer がコード生成する際に、単に AST を作るだけじゃなくて型チェックなどまで行っていて、その中の import チェックでこけています。
GOPATH が通ってるのに import がコケてるのは、 golang.org/x/tools/gcimporter
が $GOPATH/pkg/
配下から $GOPATH/pkg/pkg.a
などのファイルを探していて、 Go のソースファイルは探していないせいです。
憶測ですが、Go のパッケージ名は慣習的にディレクトリ名と同じになっていますが、実際には package 文にはディレクトリ名以外の名前を指定することもできるため、コンパイラを呼び出さずに高速に解析するための制限なのかもしれません。
解決策1: go install する
上の例で言えば、 go generate pkg
の前に go install pkg/sub
しておけば sub.a が生成され、 import エラーが無くなります。
個人的にはビルド手順が増えてしまうのが嫌なのですが、ビルド高速化のためなどにプロジェクト全体ではなくパッケージ単位でビルドしている場合は自然にこの方法でエラーが回避できているかもしれません。
解決策2: ファイルを分割し、 stringer にファイル名を指定する
stringer のフラグ以外の引数は通常省略して、カレントディレクトリをパッケージとして解析させます。
通常は go generate が stringer を実行するときにカレントディレクトリを移動するのでこれでいいのですが、これではパッケージ全体から -type
に指定した型を検索することになり、そのパッケージ内に1つでも install されてないパッケージの import があるとエラーになってしまいます。
そこで stringer のドキュメント を読んでみると、引数の説明は次のようになっています。
With no arguments, it processes the package in the current directory. Otherwise, the arguments must name a single directory holding a Go package or a set of Go source files that represent a single Go package.
最後に "a set of Go source files that represent a single Go package" とありますが、 -type
で指定した型の定義が読み込まれつつ静解析を通りさえすれば、本当のパッケージのソースコード全体を指定する必要はありません。
stringer をかけたい定数だけを集めたソースコードを切り分けておけば、そのソースコードは外部のモジュールを一切 import せず、かつ同一パッケージ内の他のファイルの定義も参照しないので、1ファイルでパッケージとしてコンパイルすることが可能になります。
上の例で言えば、次のようにファイルを分割することで、無事 go generate が成功します。
package pkg
//go:generate stringer -type=aaa aaa.go
type aaa byte
const (
a aaa = iota
b
c
d
)
package pkg
import "pkg/sub"
func Xyzzy(a aaa, b sub.bbb) {
}
個人的にはこの方法を一番おすすめします。
解決策3: package を分ける
go の const を enum 代わりに使う場合、 C# などに比べると型がenum値の名前空間にならない (aaa.a
ではなく単なる a
になる) ので、名前が衝突して困ることがあります。
go の標準ライブラリなどは慣習的にパッケージの粒度が比較的大きく、定数セットは prefix を付けて区別しているのですが、プライベートなプロジェクトでは enum ごとに型名と同じパッケージを作ってしまうのもいいかもしれません。
(その場合、値は aaa.a
のようになる代わりに、型は aaa.aaa
になってしまうのがあまりきれいではないですが。)