Go Guidelines for Coding Agents: AIにプロダクショングレードのGoを書かせる
概要¶
AIコードエージェント(Claude、Copilot、Cursorなど)にGoを書かせると、動くけどGoらしくないコードや、パフォーマンス・安全性の問題があるコードが生成されやすい。
このガイドラインをシステムプロンプトや CLAUDE.md に含めることで、プロダクション品質のGoを生成させることを目的としている。
https://github.com/mhmtszr/go-guidelines
エラーハンドリング¶
// NG: エラーを無視
result, _ := someFunction()
// OK: エラーを必ず処理。%w でラップしてコンテキストを付ける
result, err := someFunction()
if err != nil {
return fmt.Errorf("someFunction failed for userID %d: %w", userID, err)
}
fmt.Errorf("%w", err) でラップすると errors.Is() / errors.As() でアンラップできる。
errors.New() で新しいエラーを返したり、ラップ忘れで情報が消えるのが典型的なNG。
goroutine リーク防止¶
// NG: goroutineが永遠に走り続ける可能性がある
go func() {
for {
process()
}
}()
// OK: context でキャンセルできるようにする
go func(ctx context.Context) {
for {
select {
case <-ctx.Done():
return
default:
process()
}
}
}(ctx)
goroutineはプロセスが終わるまで生き続けるので、止める手段がないとメモリリークになる。
Context以外にも done chan struct{} を使う方法もある。
インターフェースは使う側で定義する¶
// NG: 実装側(提供側)でインターフェースを定義する
// userrepo/repo.go
type UserRepository interface {
Find(id int) (*User, error)
Save(user *User) error
}
// OK: 使う側(消費側)で必要なメソッドだけ定義する
// service/user_service.go
type userFinder interface { // 小文字: このパッケージ内専用
Find(id int) (*User, error)
}
func NewUserService(repo userFinder) *UserService { ... }
なぜこれがGoらしいのか: Goのインターフェースは暗黙的(implements キーワードが不要)。
使う側が必要なメソッドだけを宣言すれば、実装側は何も知らなくてよい。
テスト時にモックしやすくなり、依存が最小になる。
小さい構造体は値渡し¶
// NG: 小さい構造体にポインタを使う → GCプレッシャーが増える
func process(p *Point) { ... }
func distance(a, b *Point) float64 { ... }
// OK: 小さい構造体は値渡し → スタックに乗ってヒープアロケーションが起きない
type Point struct{ X, Y float64 } // 16バイト
func process(p Point) { ... }
func distance(a, b Point) float64 { ... }
目安: 64バイト以下の構造体は値渡しの方が速いことが多い。
time.Time(24バイト)も標準ライブラリは値渡しで設計されている。
channel の正しいパターン¶
// NG: 受信側でcloseしている(送信側がcloseすべき)
ch := make(chan int)
go func() {
for v := range ch {
fmt.Println(v)
}
}()
close(ch) // ← 受信側(goroutine)がまだ使っているのにcloseするとpanicの可能性
// OK: 送信側がcloseする
ch := make(chan int, 10)
go func() {
defer close(ch) // 送信が終わったらclose
for _, v := range data {
ch <- v
}
}()
for v := range ch {
fmt.Println(v)
}
channelの原則: 送信側がcloseする。受信側は range で受け取るとcloseを自動検知して終了できる。
構造体の初期化: フィールド名を明示する¶
// NG: フィールド名なしの初期化(フィールドを追加/並び替えしたときにバグる)
user := User{"Alice", 30, "alice@example.com"}
// OK: フィールド名を明示
user := User{
Name: "Alice",
Age: 30,
Email: "alice@example.com",
}
テストコードでも同様。User{"Alice", 30} は後でフィールド追加したとき暗黙的に壊れる。
テスト: テーブル駆動テスト¶
// NG: 個別テストを並べる
func TestAdd(t *testing.T) {
if Add(1, 2) != 3 {
t.Error("1+2 should be 3")
}
if Add(-1, 1) != 0 {
t.Error("-1+1 should be 0")
}
}
// OK: テーブル駆動テスト
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"positive", 1, 2, 3},
{"zero sum", -1, 1, 0},
{"both negative", -2, -3, -5},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Add(tt.a, tt.b)
if got != tt.expected {
t.Errorf("Add(%d, %d) = %d, want %d", tt.a, tt.b, got, tt.expected)
}
})
}
}
テーブル駆動テストはGoの標準的な書き方。ケース追加が1行で済み、失敗時に name でどのケースか分かる。
init() を避ける¶
// NG: init()で副作用を持つ初期化
func init() {
db, err = sql.Open("postgres", os.Getenv("DATABASE_URL"))
if err != nil {
panic(err)
}
}
// OK: 明示的な初期化関数を作る
func NewDB(dsn string) (*sql.DB, error) {
return sql.Open("postgres", dsn)
}
init() は呼び出し順が不明確で、テストで初期化をコントロールしにくくなる。
依存を明示的に渡す(Dependency Injection)方が保守しやすい。
名前付き戻り値は使わない(基本的に)¶
// NG: 名前付き戻り値 + 裸のreturn(何を返しているか不明)
func divide(a, b float64) (result float64, err error) {
if b == 0 {
err = errors.New("division by zero")
return // ← 何を返しているか追わないと分からない
}
result = a / b
return
}
// OK: 通常の戻り値
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
例外: deferで戻り値を修正するケース(エラーのラッピング)では名前付き戻り値が有用。
なぜAIエージェントへの明示的な指示が必要か¶
AIは「動くコード」を生成するように最適化されているが、「Goらしいコード」を生成するとは限らない。 学習データに古いGoコード(Go 1.10以前の書き方)が多く含まれているため、モダンなパターンを知らないことがある。
CLAUDE.md や .cursorrules にこのリストを含めておくと、コードレビューで指摘するコストが大幅に減る。