mcpField
G
golang-idioms

golang-idioms

github.com/rohitg00開発2.8k189 回インストール24 週間前に更新
リポジトリ

golang-idioms について

Go 開発者向けのコーディングリファレンススキルです。エラー、インターフェース、並行処理、テスト、モジュール、ロギングの慣用法に加え、コンテキスト伝播と型設計の指針も示します。

主な機能

  1. 01コンテキスト付きエラー処理
  2. 02小さく組み合わせ可能なインターフェース
  3. 03ゴルーチンとチャネルのパターン
  4. 04コンテキスト伝播
  5. 05テーブル駆動テストとヘルパー
  6. 06モジュール・型・ロギングの指針

活用シーン

  1. 01Go のエラー処理の確認
  2. 02キャンセル可能な並行処理の設計
  3. 03テーブル駆動テストの構成
  4. 04モジュールと型設計の確認

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/rohitg00/awesome-claude-code-toolkit --skill golang-idioms

インストール後、Claude Code で/golang-idiomsを入力して読み込まれたか確認します。

詳細

Go のイディオム

エラー処理

// Return errors, never panic in library code
func LoadConfig(path string) (Config, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return Config{}, fmt.Errorf("reading config %s: %w", path, err)
    }

    var cfg Config
    if err := json.Unmarshal(data, &cfg); err != nil {
        return Config{}, fmt.Errorf("parsing config: %w", err)
    }

    return cfg, nil
}

ルール:

  • fmt.Errorf("context: %w", err) を使って、常にエラーをコンテキスト付きでラップします
  • 呼び出し元が errors.Is と errors.As を使えるように、%w を使用します
  • 適切なレベルでエラーを処理します。同じエラーをログに記録してから返してはいけません
  • 想定される条件に対してセンチネルエラーを定義します
var (
    ErrNotFound    = errors.New("not found")
    ErrUnauthorized = errors.New("unauthorized")
)

func GetUser(id string) (User, error) {
    user, ok := store[id]
    if !ok {
        return User{}, fmt.Errorf("user %s: %w", id, ErrNotFound)
    }
    return user, nil
}

// Caller
user, err := GetUser(id)
if errors.Is(err, ErrNotFound) {
    http.Error(w, "user not found", http.StatusNotFound)
    return
}

インターフェース設計

// Keep interfaces small (1-3 methods)
type Reader interface {
    Read(p []byte) (n int, err error)
}

type UserStore interface {
    GetUser(ctx context.Context, id string) (User, error)
    CreateUser(ctx context.Context, u User) error
}

// Accept interfaces, return structs
func NewService(store UserStore, logger *slog.Logger) *Service {
    return &Service{store: store, logger: logger}
}

ルール:

  • インターフェースは実装する側ではなく、使用する側(コンシューマー側)で定義します
  • 大きなインターフェースよりも、小さく組み合わせ可能なインターフェースを優先します
  • 標準ライブラリの io.Reader、io.Writer、fmt.Stringer を使用します
  • メソッドが 1 つのインターフェースには、メソッド名に er を付けた名前を付けます

ゴルーチンとチャネルのパターン

ワーカープール
func process(ctx context.Context, jobs <-chan Job, workers int) <-chan Result {
    results := make(chan Result, workers)
    var wg sync.WaitGroup

    for range workers {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for job := range jobs {
                select {
                case <-ctx.Done():
                    return
                case results <- job.Execute():
                }
            }
        }()
    }

    go func() {
        wg.Wait()
        close(results)
    }()

    return results
}
ファンアウト/ファンイン
func fanOut[T, R any](ctx context.Context, items []T, fn func(T) R, concurrency int) []R {
    sem := make(chan struct{}, concurrency)
    results := make([]R, len(items))
    var wg sync.WaitGroup

    for i, item := range items {
        wg.Add(1)
        sem <- struct{}{}
        go func() {
            defer func() { <-sem; wg.Done() }()
            results[i] = fn(item)
        }()
    }

    wg.Wait()
    return results
}

ルール:

  • 常に context.Context を最初のパラメーターとして渡します
  • コンテキストのキャンセルまたはチャネルのクローズによって、ゴルーチンを常に停止できるようにします
  • ゴルーチンの完了を待つには sync.WaitGroup を使用します
  • プロデューサーとコンシューマーの処理速度が異なる場合は、バッファー付きチャネルを使用します
  • 停止方法がわからないゴルーチンを起動してはいけません

コンテキストの伝播

func (s *Service) HandleRequest(ctx context.Context, req Request) (Response, error) {
    ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
    defer cancel()

    user, err := s.store.GetUser(ctx, req.UserID)
    if err != nil {
        return Response{}, fmt.Errorf("getting user: %w", err)
    }

    ctx = context.WithValue(ctx, userKey, user)
    return s.processRequest(ctx, req)
}

ルール:

  • I/O を行うすべての関数で、コンテキストを最初のパラメーターとして渡します
  • すべての外部呼び出しで context.WithTimeout または context.WithDeadline を使用します
  • キャンセル可能なコンテキストを作成した後は、常に defer cancel() を実行します
  • context.WithValue の使用は必要最小限にします(トレース ID や認証情報など、リクエストスコープの値に限ります)
  • コンテキストを構造体に保存してはいけません

テーブル駆動テスト

func TestValidateEmail(t *testing.T) {
    tests := []struct {
        name  string
        email string
        want  bool
    }{
        {"valid email", "[email protected]", true},
        {"missing @", "userexample.com", false},
        {"empty string", "", false},
        {"multiple @", "user@@example.com", false},
        {"valid with subdomain", "[email protected]", true},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got := ValidateEmail(tt.email)
            if got != tt.want {
                t.Errorf("ValidateEmail(%q) = %v, want %v", tt.email, got, tt.want)
            }
        })
    }
}
テストヘルパー
func newTestServer(t *testing.T) *httptest.Server {
    t.Helper()
    handler := setupRoutes()
    srv := httptest.NewServer(handler)
    t.Cleanup(srv.Close)
    return srv
}

func assertEqual[T comparable](t *testing.T, got, want T) {
    t.Helper()
    if got != want {
        t.Errorf("got %v, want %v", got, want)
    }
}

すべてのテストユーティリティ関数で t.Helper() を使用します。テストリソースのクリーンアップには defer ではなく t.Cleanup() を使用します。テスト用のフィクスチャには testdata/ ディレクトリを使用します。

モジュール管理

go.mod structure:
module github.com/org/project

go 1.23

require (
    github.com/lib/pq v1.10.9
    golang.org/x/sync v0.7.0
)

コマンド:

go mod tidy          # remove unused, add missing
go mod verify        # verify checksums
go list -m -u all    # check for updates
go get -u ./...      # update all dependencies
go mod vendor        # vendor dependencies (optional)

コミットする前に、必ず go mod tidy を実行します。メジャーバージョンを固定します。更新前に変更履歴を確認します。

ゼロ値を活用した設計

ゼロ値が有用になるように型を設計します。

// sync.Mutex zero value is an unlocked mutex (ready to use)
var mu sync.Mutex

// bytes.Buffer zero value is an empty buffer (ready to use)
var buf bytes.Buffer
buf.WriteString("hello")

// Custom types: make zero value meaningful
type Server struct {
    Addr    string        // defaults to ""
    Handler http.Handler  // defaults to nil
    Timeout time.Duration // defaults to 0 (no timeout)
}

func (s *Server) ListenAndServe() error {
    addr := s.Addr
    if addr == "" {
        addr = ":8080" // useful default
    }
    handler := s.Handler
    if handler == nil {
        handler = http.DefaultServeMux
    }
    // ...
}

ルール:

  • コンストラクターよりも、意味のあるゼロ値を持つ構造体を優先します
  • メソッドでレシーバーを変更する場合は、ポインターレシーバーを使用します
  • メソッドで読み取りのみを行う場合は、値レシーバーを使用します
  • ユーザーが直接設定すべきでないフィールドをエクスポートしてはいけません。代わりにコンストラクター関数を使用します

構造化ログ

import "log/slog"

logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
    Level: slog.LevelInfo,
}))

logger.Info("request handled",
    slog.String("method", r.Method),
    slog.String("path", r.URL.Path),
    slog.Int("status", status),
    slog.Duration("latency", time.Since(start)),
)

log/slog(標準ライブラリ、Go 1.21 以降)を使用します。構造化フィールドを使用し、文字列補間は使用しません。すべてのログエントリーにリクエスト ID、ユーザー ID、操作名を含めます。

よくあるアンチパターン

  • 具体的な型ではなく interface{} / any を返す
  • 複雑なセットアップに init() を使用する(テストが難しくなります)
  • コメントなしで _ を使ってエラーを無視する
  • ライフサイクルを管理せずにゴルーチンを使用する
  • ロック範囲が広すぎることによるミューテックスの競合
  • チャネルの誤用:単純な共有状態にはミューテックスを優先する
  • 数行を超える関数で名前付き戻り値を使わない return を使用する

コメント