コンテンツにスキップ

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 にこのリストを含めておくと、コードレビューで指摘するコストが大幅に減る。