Go上級 #7 コード生成 — go generate と stringer
#6 プロファイリングで計測ツールを見たなら — 今回は計測結果でよく見つかるケース。reflectがホットパスにあるときどう速くするか。
Goの答えは — コード生成。ランタイムにreflectで分析する代わりに、コンパイル時にあらかじめコードを作っておく道。Go上級シリーズの最後の記事です。
go generateの全体像
#
go generateは — ソースの中の特別なコメントに出会うとコマンドを実行するツール。
//go:generate stringer -type=Status
package main
type Status int
const (
Pending Status = iota
Active
Closed
)go generate ./...このコマンドが — 上のコメントを発見してstringer -type=Statusを実行。そのツールが新しいファイルを生成します。
結果 — 自動生成ファイル #
stringerが作り出したstatus_string.go:
// Code generated by "stringer -type=Status"; DO NOT EDIT.
package main
func (i Status) String() string {
switch i {
case Pending:
return "Pending"
case Active:
return "Active"
case Closed:
return "Closed"
}
return "Status(...)"
}これでStatusが — fmt.Stringerを満たします。fmt.Println(s)なら「Active」のような人が読みやすい文字列。
なぜ reflect より良いのか? #
同じことをreflectでもできます。enumの名前を動的に — しかし:
| 比較 | reflect | code gen |
|---|---|---|
| 速度 | 〜100x 遅い | 通常コードと同じ |
| コンパイル時検証 | なし | あり |
| デバッグ | 難しい | 易しい |
| ビルド複雑度 | 単純 | 生成段階追加 |
ホットパスでreflectを使っていた箇所を — コンパイル時情報で解けるなら常により良いです。
stringer — enum 表現 #
go install golang.org/x/tools/cmd/stringer@latest//go:generate stringer -type=Status,Priority
type Status int
const (
Pending Status = iota
Active
)
type Priority int
const (
Low Priority = iota
High
)生成ファイル: status_string.go、priority_string.go。enumが追加されるたびに — go generate一発で新しく作られます。
mockgen — インターフェースのモッキング #
go.uber.org/mockのmockgenが — インターフェースからmockを自動生成。
//go:generate mockgen -source=db.go -destination=mock_db.go -package=mocks
type DB interface {
Get(id string) (User, error)
Save(u User) error
}生成されたmockで — テストで動作を事前定義可能。
mockDB := mocks.NewMockDB(ctrl)
mockDB.EXPECT().Get("u1").Return(User{Name: "イ・ドギョン"}, nil)
svc := NewService(mockDB)
svc.Greet("u1")reflectで動的mockを作ることもできますが — 自動生成のほうがIDEオートコンプリート、コンパイル型チェック、デバッグすべてで優れています。
自作 — text/templateで
#
よく登場するパターン。
//go:build ignore
package main
import (
"os"
"text/template"
)
const tmpl = `// DO NOT EDIT.
package main
func {{.Name}}Greet() string {
return "Hello, {{.Name}}!"
}
`
func main() {
t := template.Must(template.New("g").Parse(tmpl))
f, _ := os.Create("greet_gen.go")
t.Execute(f, map[string]string{"Name": "World"})
}//go:generate go run generator.go//go:build ignoreタグがあるので — 通常のビルドからは除外、go run generator.goでだけ実行。
パターン — シリアライゼーション高速化 #
大きなプロジェクトでよく見る場面。encoding/jsonのreflectコストを — コード生成で回避。
ツール:
mailru/easyjson— struct → 専用マーシャラーコードpquerna/ffjson— 似たコンセプトmvdan/gogenerate、vektra/mockery— 多様な分野
//go:generate easyjson -all user.go
//easyjson:json
type User struct {
Name string `json:"name"`
Email string `json:"email"`
}生成されたuser_easyjson.go — User型専用のMarshalJSON / UnmarshalJSON。reflectなしで直接byteを扱うので速いです。
ベンチマーク結果は通常 — encoding/jsonより2〜5倍速い場面もあります。ただし — ビルド段階が増え、自動生成コードがPRに混じるトレードオフ。
protobuf / gRPC — 自動生成の頂点 #
protoc --go_out=. user.proto.protoファイルから — Go struct、マーシャラー、gRPCサーバー/クライアントコードすべてを自動生成。実戦で見る最大のコード生成事例。
user.pb.go ← struct + Marshal/Unmarshal
user_grpc.pb.go ← gRPC サーバー/クライアント数万行のコードが — .proto1ファイルから。このパイプラインがマイクロサービスの標準ツール。
実戦ワークフロー #
Makefile にまとめる #
.PHONY: gen
gen:
go generate ./...
.PHONY: build
build: gen
go build ./...
生成をビルドの前段階として。ただし、自動生成ファイルは — 通常gitにコミットしておきます(他の開発者がツールなしでもビルドできるように)。
CI で検証 #
生成ファイルが — 常に最新かを確認。
go generate ./...
git diff --exit-code # 生成後に変更があれば失敗PRに新しい自動生成ファイルが抜けていればCIが捕まえてくれます。
どこまで自動生成するか #
回避すべき場面:
- ロジックが頻繁に変わるコード — 毎回生成段階を経ると負担
- PRレビューが難しい大きな生成ファイル — 本当の変更が埋もれる
- 外部ツール依存が負担な小さなプロジェクト
自然な場面:
- enumのStringメソッド、JSONマーシャラー — 静的情報ベース
- インターフェースmock
- protobuf / OpenAPIのようなスペック → コード変換
ルールは単純 — 同じパターンを手で5回以上書くようになれば自動生成検討の時点。
罠 — 生成ファイルを直接修正 #
// Code generated by ...; DO NOT EDIT.このヘッダが見えれば — 絶対に直接編集禁止。次のgenerateで上書きされます。本当に修正が必要なら — 生成ツール自体またはテンプレートを修正。
罠 — 生成ツールのバージョン固定 #
//go:build tools
// +build tools
package tools
import (
_ "golang.org/x/tools/cmd/stringer"
_ "go.uber.org/mock/mockgen"
)tools.goに — 使用ツールをimport(build tagで通常ビルドから除外)。go.modがバージョンを固定。チーム全体が同じツールバージョンを保証。
シリーズ総まとめ — Go上級7編を振り返って #
7編をまとめると:
- 並行性パターン — pipeline、fan-out/fan-in、semaphore
- メモリモデルとsync — Mutex、atomic、Once
- ジェネリクス — type parameter、constraint
- reflect — ランタイム型を扱う
- unsafeとcgo — 安全領域の外
- プロファイリング — pprof、benchmark、trace
- コード生成 (この記事)
基礎/中級で見たツールの限界が見える場面 — 並行性をうまく書くパターン、共有メモリの同期、ジェネリクスで多態性を表現、reflect/unsafe/cgoで境界を渡る、計測と自動化。これらのツールが一緒に登場する場面は通常 — フレームワークやライブラリのコード。
次のステップ #
次のシリーズ(#1 はじめてのHTTPサーバー)はGo実戦6編。ここまで学んだツールを — 実際のバックエンドを書きながらどう組み立てるかを整理します。net/http、ServeMux、JSON、DB連携、ミドルウェア、テストとデプロイまで。
まとめ #
今回の記事で整理した内容:
//go:generate+go generate ./...— コンパイル時コード生成の標準- stringer — enumのStringメソッド
- mockgen — インターフェースmock
- easyjson / protoc — シリアライゼーション高速化、RPCコード
- 自動生成ファイルは — gitにコミット、直接編集禁止、CIで最新かを検証
- ツールバージョンは —
tools.goで固定 - reflectがホットパスにあるなら — コード生成を検討
これでGo上級7編は終わりです。