Go上級 #7 コード生成 — go generate と stringer

#6 プロファイリングで計測ツールを見たなら — 今回は計測結果でよく見つかるケース。reflectがホットパスにあるときどう速くするか

Goの答えは — コード生成。ランタイムにreflectで分析する代わりに、コンパイル時にあらかじめコードを作っておく道。Go上級シリーズの最後の記事です。

go generateの全体像 #

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の名前を動的に — しかし:

比較reflectcode gen
速度〜100x 遅い通常コードと同じ
コンパイル時検証なしあり
デバッグ難しい易しい
ビルド複雑度単純生成段階追加

ホットパスでreflectを使っていた箇所を — コンパイル時情報で解けるなら常により良いです。

stringer — enum 表現 #

stringer インストール
go install golang.org/x/tools/cmd/stringer@latest
複数 enum
//go:generate stringer -type=Status,Priority

type Status int
const (
	Pending Status = iota
	Active
)

type Priority int
const (
	Low Priority = iota
	High
)

生成ファイル: status_string.gopriority_string.go。enumが追加されるたびに — go generate一発で新しく作られます。

mockgen — インターフェースのモッキング #

go.uber.org/mockmockgenが — インターフェースから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で — テストで動作を事前定義可能。

mock の使用
mockDB := mocks.NewMockDB(ctrl)
mockDB.EXPECT().Get("u1").Return(User{Name: "イ・ドギョン"}, nil)

svc := NewService(mockDB)
svc.Greet("u1")

reflectで動的mockを作ることもできますが — 自動生成のほうがIDEオートコンプリート、コンパイル型チェック、デバッグすべてで優れています。

自作 — text/template#

よく登場するパターン。

generator.go
//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/gogeneratevektra/mockery — 多様な分野
easyjson 使用例
//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 にまとめる #

Makefile
.PHONY: gen
gen:
	go generate ./...

.PHONY: build
build: gen
	go build ./...

生成をビルドの前段階として。ただし、自動生成ファイルは — 通常gitにコミットしておきます(他の開発者がツールなしでもビルドできるように)。

CI で検証 #

生成ファイルが — 常に最新かを確認。

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編をまとめると:

  1. 並行性パターン — pipeline、fan-out/fan-in、semaphore
  2. メモリモデルとsync — Mutex、atomic、Once
  3. ジェネリクス — type parameter、constraint
  4. reflect — ランタイム型を扱う
  5. unsafeとcgo — 安全領域の外
  6. プロファイリング — pprof、benchmark、trace
  7. コード生成 (この記事)

基礎/中級で見たツールの限界が見える場面 — 並行性をうまく書くパターン、共有メモリの同期、ジェネリクスで多態性を表現、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編は終わりです。

X