LoginSignup
17
15

More than 5 years have passed since last update.

stringer を使う場合は import に注意

Last updated at Posted at 2015-01-19

Go 1.4 が出て go generate コマンドが登場しました。 go generate コマンドの代表的な利用例として紹介されているのが stringer です。 新しもの好きな人は定数にどんどん stringer をかけていってることでしょう。

そんな stringer ですが、次のようなケースでちょっとハマりました。

src/pkg/aaa.go
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) {
}
src/pkg/sub/bbb.go
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 が成功します。

src/pkg/aaa.go
package pkg

//go:generate stringer -type=aaa aaa.go
type aaa byte

const (
    a aaa = iota
    b
    c
    d
)
src/pkg/xyz.go
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 になってしまうのがあまりきれいではないですが。)

17
15
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
15