mcpField
G
go

go

github.com/spf13開発695245 回インストール2 週間前に更新
リポジトリ

go について

goスキルは、Go開発者向けに慣用的なコードの作成、レビュー、リファクタリングを支援します。機能を提案する前にモジュールの最低バージョンを確認し、明快な設計と確実なエラー処理を重視します。

主な機能

  1. 01Goのバージョンと機能の確認
  2. 02慣用コードとエラー処理の指針
  3. 03パッケージとAPIの設計支援
  4. 04標準ライブラリと依存関係の選択
  5. 05並行処理とテストの実践

活用シーン

  1. 01Goコードの新規作成
  2. 02既存Goプロジェクトのレビューやリファクタリング
  3. 03Goパッケージ、モジュール、APIの設計
  4. 04互換性やテストに関する問題の確認

インストール

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

$npx skills add https://github.com/spf13/go-skills --skill go

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

同梱ファイル.claude-plugin/plugin.json

詳細

Go らしい書き方:Go の流儀

堅牢で効率的、保守しやすいアプリケーションを構築するための、Go らしいパターンとベストプラクティスです。

有効にするタイミング

  • 新しい Go コードを書くとき
  • 既存の Go コードをレビューまたは監査するとき
  • Go コードをリファクタリングするとき(特に、Go で Java/Spring Boot のようなパターンに見えるコード)
  • Go のパッケージ、モジュール、または API を設計するとき
  • 標準ライブラリとサードパーティーライブラリのどちらを選ぶか判断するとき
  • Go プロジェクトの構成、エラー処理、並行処理、テストに関する質問に答えるとき

バージョンの条件:まずモジュールの下限を確認する

バージョンに依存する機能はすべて、下限として扱います。モジュールのパッケージは、その機能が導入されたリリースでコンパイルできなければなりません。そのような機能を推奨する前に下限を確認し、対応している機能だけを推奨してください。

  1. モジュールの go.mod にある go ディレクティブを読みます。そのバージョンがモジュール自身のパッケージの下限です。go 1.21 を宣言しているモジュールは、使用中のツールチェーンにかかわらず、Go 1.21 でコンパイルできなければなりません。
  2. ファイルのビルド制約を確認します。//go:build go1.24 の指定があるファイルでは、モジュールの go ディレクティブがそれより古くても、Go 1.24 の機能を使用できます。
  3. 下限が不明な場合は、go.mod を読むか、確認してください。推測したり、読み手が最新リリースを使っていると決めつけたりしてはいけません。

逆の誤りも同じように問題です。下限がすでに許容しているモジュールで、イディオムが利用できないと指摘してはいけません。また、便利なヘルパー関数を使うためだけにモジュールの go ディレクティブを引き上げてはいけません。go ディレクティブで指定する下限は、モジュールの依存先すべてに課されます。

バージョン → 機能インデックス

機能ごとに1行で記載しています。利用可能かどうかは推測せず、ここで確認してください。

開始バージョン機能
1.17//go:build 制約。// +build 行の代わりに必須
1.18型パラメーター(ジェネリクス)
1.18interface{} のエイリアスとしての any
1.19sync/atomic の型付き値(atomic.Int64、atomic.Pointer[T])
1.20errors.Join
1.21slices パッケージ
1.21maps パッケージ
1.21cmp パッケージ(cmp.Compare)
1.21組み込み関数 min、max、clear
1.21log/slog
1.21context.WithoutCancel
1.22math/rand/v2
1.22net/http ServeMux のメソッドパターンとワイルドカードパターン
1.22for i := range n(整数に対する range)
1.22ループごとの反復変数
1.22slices.Concat
1.22cmp.Or
1.23関数を範囲指定するイテレーター(iter.Seq、iter.Seq2)
1.23イテレーターとしての maps.Keys / maps.Values
1.23slices.Collect / slices.Values / slices.Sorted
1.24encoding/json の omitzero
1.24go.mod の tool ディレクティブ
1.24ジェネリック型エイリアス
1.24t.Context()、b.Loop()、t.Chdir()
1.25testing/synctest(synctest.Test)
1.25sync.WaitGroup.Go
1.26new(expr)
1.26自己参照型パラメーター制約
1.26errors.AsType[T]
1.26log/slog.NewMultiHandler
1.26reflect のイテレーター(Type.Fields、Type.Methods、Type.Ins、Type.Outs、Value.Fields、Value.Methods)
1.26go test -artifacts を使った t.ArtifactDir() / b.ArtifactDir() / f.ArtifactDir()
1.26B.Loop がインライン化を妨げなくなる
1.26testing/cryptotest.SetGlobalRandom
1.26Green Tea GC がデフォルトで有効
1.2664ビットプラットフォームでのヒープベースアドレスのランダム化
1.26runtime/pprof の goroutineleak プロファイル(実験機能:GOEXPERIMENT=goroutineleakprofile)
1.26simd/archsimd(実験機能:GOEXPERIMENT=simd)
1.26runtime/secret(実験機能:GOEXPERIMENT=runtimesecret)
1.27ジェネリックメソッド
1.27構造体リテラルのキーとして任意のフィールドセレクターを使用可能
1.27関数型の一般化された型推論
1.27go test で stdversion vet チェックがデフォルトで有効
1.27uuid パッケージ
1.27strings.CutLast / bytes.CutLast
1.27net/url の URL.Clone / Values.Clone
1.27math/rand/v2 の (*Rand).N
1.27testing/synctest.Sleep
1.27net/http/httptest.NewTestServer
1.27runtime/pprof の goroutineleak プロファイルが一般利用可能に
1.27time のタイマーとティッカーが常にバッファーなしに。asynctimerchan は削除
1.27サイズに特化した小規模アロケーション(80バイト未満)
1.27encoding/json が encoding/json/v2 の実装を基盤とするように
1.27simd(実験機能:GOEXPERIMENT=simd)
1.27arm64 と WebAssembly での simd/archsimd(実験機能:GOEXPERIMENT=simd)
適用:stdversion

Go 1.27 以降、go test はデフォルトで stdversion vet チェックを実行します。参照元ファイルに適用される Go バージョンに対して新しすぎる標準ライブラリのシンボルの使用を検出します。適用されるバージョンは、go.mod の go ディレクティブとファイルのビルドタグによって決まります。

  • バージョン制約のあるコードが正しいと判断する前に、go vet ./...(または go test ./...)を実行してください。
  • stdversion の指摘は誤検知ではなく、バージョン制約のエラーとして扱ってください。コードがモジュールの最低バージョンに対して新しすぎるか、最低バージョンを意図的に引き上げており、モジュールに依存するすべての利用者に知らせる必要があります。
  • 対象となるのは標準ライブラリのシンボルのみです。言語機能(new(expr)、ジェネリックメソッド)や、新しいシンボルを導入しない動作変更については、引き続き自分でバージョンを制約してください。

基本原則

1. 明快さは巧妙さに勝る

Go は抽象化や巧妙さより、読みやすさとシンプルさを重視します。コードは見ただけで明らかに理解できるべきです。制御フローを理解するために関数を3回読まなければならないなら、書き直す必要があります。

// Idiomatic: Direct, linear control flow. Note: no "Get" prefix — Go omits it.
func LookupUser(id string) (*User, error) {
    user, err := db.FindUser(id)
    if err != nil {
        return nil, fmt.Errorf("finding user %s: %w", id, err)
    }
    return user, nil
}
2. ゼロ値をそのまま使えるようにする

初期化せずにすぐ使えるよう、型を設計します。これにより、定型的なコンストラクターが不要になります。sync.Mutex と bytes.Buffer が理想的な例です。

// Idiomatic: Ready to use immediately
type Counter struct {
    mu    sync.Mutex
    count int
}

func (c *Counter) Inc() {
    c.mu.Lock()
    c.count++
    c.mu.Unlock()
}
3. 早めに返して、ハッピーパスを左端に保つ

エラーや例外的なケースはすぐに処理して、戻りましょう。メインロジックに else ブロックを使わないでください。関数の「ハッピーパス」はインデントされていない状態にします。

コードを減らす:必要性のはしご

コードを書く前に、最初に当てはまる段で止まりましょう。問題を理解してから、はしごを上ってください。まず変更対象のコードを読み、実際の処理の流れを追います。2つの段で実現できるなら、より上の段を選んで先に進みましょう。

  1. これは存在する必要がありますか? 「後で必要になる」は必要性ではありません。実装せず、その理由を一行で伝えましょう。
  2. コードベースにすでにありますか? 書く前に検索しましょう。ヘルパー、型、パターンが、1つ先のパッケージにすでにあるかもしれません。すでにあるものを再実装することが、この間違いの最も一般的な形です。
  3. 標準ライブラリでできますか? Go では、ほかのどの段よりもここで多くのことを解決できます。ルーターより net/http の ServeMux、ロギングモジュールより slog、手書きのループより slices/maps/cmp、multierr より errors.Join、RNG ヘルパーより math/rand/v2 を使いましょう。標準ライブラリ:新しいパッケージを使うを参照してください。
  4. ツールチェーンやプラットフォームに、すでに機能がありますか? これらは追加コストなしで使え、すでにインストールされています。アセットパイプラインやローダーより //go:embed、手書きのパス制限より os.Root、遅延初期化ヘルパーより sync.OnceValue、スクリプトより tool ディレクティブ、シェルラッパーよりビルドタグ、アプリケーションコードで再実装するより CHECK 制約や一意インデックスを使いましょう。サービスのテンプレートでは、コンポーネントライブラリとそのバンドルより、ネイティブの入力要素が適しています。
  5. すでに go.mod にありますか? すでに導入済みの依存関係を使いましょう。数行のコードや既存のインポートで実現できることのために、モジュールを追加してはいけません。ただし、標準ライブラリだけを使うというルールではありません。golang.org/x/sync/errgroup、afero、go-cmp は導入済みの依存関係であり、このスキルで推奨しています(並行処理のパターンとテストのパターンを参照)。手書きで再実装するものではありません。
  6. 一行で書けますか? その一行を書きましょう。
  7. それから初めて: 動作する最小限のものを実装しましょう。余分なファイル、レイヤー、設定項目は不要です。実装が1つしかないインターフェース、製品が1つしかないファクトリー、値が変わることのない設定フィールドは、いずれも段7での失敗です。

段3と段4にはバージョンに依存する機能が含まれます(sync.OnceValue は Go 1.21、os.Root と tool ディレクティブは Go 1.24 で導入されています)。そのため、推奨する前にモジュールの最低バージョンを確認してください。バージョンの確認を参照してください。

決して省略してはいけないもの: 信頼境界での検証、データ損失を防ぐエラー処理、セキュリティ、クリーンアップ、明示的に要求されたもの。Go では、エラーを必ず確認してラップし、リソースを解放する defer を残し、マップを安全に扱うミューテックスを維持してください。また、複雑な分岐やパーサーには、実行可能なテストを少なくとも1つ残してください(テストのパターンを参照)。手を抜くことで短くするのは解決策であって、読み込みや確認ではありません。

報告された箇所ではなく、根本原因を修正しましょう。 バグ報告にあるのは症状です。手を入れる前に、その関数を呼び出すすべての箇所を grep で確認してください。すべての呼び出し元が通る場所にガードを1つ置くほうが、各呼び出し箇所にガードを置くより差分が小さく、チケットで触れられていない呼び出し元も修正できます。

受け入れた上限を明記しましょう。 最小限の実装に既知の限界がある場合は、その限界がある箇所に記録します。// yagni: whole-table lock — per-key locks if write throughput matters。明記された上限は単純化ですが、明記されていない上限は地雷です。

パッケージ構成:まずはフラットに

アンチパターン: 深くネストしたディレクトリ構造を使ったり、「クリーンアーキテクチャ」のレイヤーを人為的に強制するために、デフォルトで internal/ フォルダーに大きく依存したりすること。これにより、循環依存や、コードの見通しの悪化を招きます。

1. 単一パッケージをデフォルトにする

まずはフラットな構成から始めます。マイクロサービスやシンプルなツールを構築する場合は、すべてをルートディレクトリ(または main.go と同じ場所)に配置します。コードを明確にするために新しい名前空間が本当に必要な場合や、厳密に独立したドメインを切り離す必要がある場合にのみ、新しいパッケージを作成します。

バイナリが1つなら、モジュールのルートに main.go を置きます。cmd/<name>/ を使うのは、モジュールが複数のバイナリを提供する場合だけです。バイナリごとにディレクトリを1つ作成し、そこにはそれ以外のものを置きません。

2. internal/ の適切な使い方

internal/ ディレクトリには、コンパイラーによる特別な制約があります。ほかのモジュールがその中のコードをインポートできないようにします。

  • アプリケーションの場合: 実行可能バイナリを構築する場合、そもそもほかの誰もコードをインポートできません。ここで internal/ を使うのは、通常、不要にパスを深くするだけです。
  • ライブラリの場合: internal/ は必要最小限に使います。自分のパッケージ間でエクスポートされた型を共有する必要がある一方で、エンドユーザーがその型に依存するのを絶対に防がなければならない複雑なサブシステムに限って使用します。
// Idiomatic: A flat, feature-focused library or simple app
myproject/
├── main.go           # Entry point (if application)
├── server.go         # Core logic
├── config.go         # Configuration
├── parser.go         # Domain specific parsing
├── parser_test.go
├── go.mod
└── go.sum
3. サービスアプリケーションのドメインパッケージ

アプリケーションに、明確に区別され、独立してテストできるドメインがある場合は、それぞれに独自のトップレベルパッケージを与えます。それでもルールは1階層までです。internal/ にネストしたり、クリーンアーキテクチャのレイヤーを設けたりしません。各パッケージが1つの関心事を担います。main.go がそれらを組み合わせます。

パッケージを作成する目安は、そのドメインを1文で説明でき、呼び出し元のパッケージを知らなくても役割を果たせるかどうかです。そうであれば、独自のパッケージにする価値があります。

// Idiomatic: A web service with distinct domain packages
myservice/
├── main.go        # wires everything together; no business logic here
├── config/        # Config struct, env loading
├── auth/          # identity verification, session middleware
├── db/            # data store client + all queries
├── storage/       # blob storage (S3, R2, GCS)
├── billing/       # payment provider + credit ledger
├── jobs/          # job lifecycle + queue dispatch + worker handler (same domain, one package)
├── web/           # HTTP handlers + HTML templates + static assets (tightly coupled by design)
├── transcribe/    # domain-specific processing — independent pure functions
├── Makefile
├── Dockerfile
└── .env.example

ドメインパッケージのルール:

  • 各パッケージには明確な目的を1つ持たせます。レイヤー名ではなく、処理内容にちなんだ名前を付けます(service/ ではなく jobs/)。
  • インポートの依存方向は一方向にします。web は jobs をインポートでき、jobs は db をインポートできますが、下位のものが上位のものをインポートしてはいけません。また、main をインポートするものもありません。同じ階層のパッケージ間のインポートは問題ありません。循環インポートと上位へのインポートが問題の兆候です。main は配線を担う場所であり、ライブラリではありません。
  • 常に一緒に扱う関連サブ関心事は、1つのパッケージにまとめます(たとえば、ジョブの作成とワーカーハンドラーは、どちらもジョブのライフサイクルというドメインを共有するため、jobs/ に置きます)。
  • HTTP ハンドラーと、それらがレンダリングするテンプレートは web/ にまとめます。設計上、両者は密接に結びついており、//go:embed が読み込めるのは自身のパッケージディレクトリ内のファイルだけなので、テンプレートと静的アセットはそこに置く必要があります。
  • utils/、helpers/、common/ パッケージは作成しません。これらは、どこが責任を持つのかが不明確な兆候です。

避けるべきアンチパターン: クリーンアーキテクチャ/DDD のレイヤー(service/、repository/、controller/、domain/)。レイヤー名のパッケージは循環インポートを引き起こし、インターフェースを増やし、Go では明確さをまったく高めません。ドメイン名のパッケージ(auth/、billing/、jobs/)が、「フラットすぎる」構成と「過剰設計」の中間にある適切な選択肢です。

4. フラットな構成が機能しなくなるとき

フラットな構成はデフォルトであり、絶対の原則ではありません。コードを見て分割すべき境界がすでに存在すると分かったら、分割します。

  • main.go が数百行を超えて増えた、または1つのファイルに無関係な関心事がいくつも含まれている。
  • 1つの構造体が無関係な2つの関心事の状態を持っている、または1つのミューテックスがその両方を保護している。
  • ある関心事をテストするために、テストを実行する前にサーバー、設定、データベースハンドルなど、プログラム全体を構築する必要がある。
  • あるファイルのインポートが分かれている。つまり、その半分はファイルの一方の処理のためだけに存在している。
  • HTTP ハンドラー、埋め込みアセット、OS へのシェル呼び出しが、それらを必要としない純粋なロジックの隣に置かれている。

これらは目標ではなく、兆候です。ドメインに沿って分割し、1階層までにします。上記の項目を満たすためにレイヤーへ分割してはいけません。隣のパッケージを変更せずには変更できないパッケージは、2つの名前を持つ1つのパッケージです。

5. 巨大なオブジェクトを分割する

手順に沿って機械的に行う場合は、次のとおりです。

  1. 状態と、それを保護するミューテックスをまとめて移動します。ロックから切り離されたフィールドは、リファクタリングではなく新たなバグです。
  2. その状態に触れるメソッドだけを移動します。それ以外は移動しません。
  3. フィールドは非エクスポートのままにします。コンストラクター(NewX)が境界です。パッケージが必要とするものを受け取り、その型を返し、ロックを隠します。
  4. main でそれを構築し、必要とする場所に渡します。新しいパッケージは、それを誰が呼び出すかを知る必要はありません。コールバックが必要なら、境界の位置が間違っています。
  5. インターフェースは利用側に置きます(インターフェース設計を参照)。新しいパッケージが、自身の呼び出し元を表すインターフェースを定義してはいけません。

テストは状態と一緒に移動します。新しいパッケージを単独でテストできないなら、分割はまだ完了していません。

インターフェース設計

1. インターフェースは事前に設計するのではなく、必要に応じて見つける

まず具象型を書きます。複数の型を利用側で相互に置き換えて使う必要があると分かったときにだけ、インターフェースを定義します。

2. インターフェースは利用する場所で定義する

インターフェースは、実装するパッケージではなく、利用するパッケージに置きます。これにより、パッケージ間の結合を弱められます。

// processor/processor.go

// Idiomatic: The consumer defines exactly what it needs.
// The concrete 'UserStore' doesn't even need to know this interface exists.
type UserFetcher interface {
    FetchUser(id string) (*User, error)
}

type Processor struct {
    fetcher UserFetcher
}
3. インターフェースを受け取り、構造体を返す

入力パラメーターには、可能な限り小さなインターフェースを要求します(例:*os.File ではなく io.Reader)。一方、呼び出し元が特定のフィールドやメソッドにアクセスするために型アサーションを強いられないよう、具象構造体を返します。

ライブラリ API の設計

ドメインオブジェクトをエントリーポイントにする

状態を持つリソース(保管庫、データベース接続、設定ストアなど)をラップする Go ライブラリを設計する場合は、そのリソースの構造体を主要なオブジェクトにします。そのメソッドから、ドメイン型のサブオブジェクトを返します。次のことは避けてください。

  • すべてのパッケージレベル関数の第 1 引数に、設定/リソース構造体を渡すこと
  • 組み込みで使われる可能性のあるライブラリコードで、パッケージレベルのグローバル状態(pflag のデフォルトの FlagSet など)を使うこと

代わりに、次のようにします。

// Open the primary resource once
v, err := vault.Open(path)

// Domain operations are methods on the primary object
// Each call is stateless (reloads fresh) — no cached state on the struct
idx, err := v.People()             // returns *people.Index, error
note, err := v.Daily(time.Now())   // returns *daily.Note, error
mtgs, err := v.Meetings()          // returns *meetings.Index, error

// Domain types live in sub-packages — callers use type inference
p, err := idx.FindOne("Steve")     // *people.Person

このパターンの理由:

  • 主要な構造体(Vault)が唯一のエントリーポイントとなり、呼び出し元は使い始めるためにインポートを 1 つ行うだけで済みます
  • サブパッケージは豊富なドメイン型(people.Person、daily.Note)を定義し、それぞれの型は、その型を所有するロジックと一緒に置かれます
  • グローバル状態がないため、ライブラリは並行利用、複数インスタンス、テストに安全です
  • 状態を持たないメソッド呼び出し(毎回最新の状態を再読み込みする)により、構造体をシンプルに保てます。キャッシュの無効化ロジックも不要です
  • 型推論(:=)により、変数宣言のためにサブパッケージを明示的にインポートする必要はほとんどありません

代わりにグローバルインスタンスを使う場合: pflag 自体のような、インスタンスが常に 1 つしか存在せず、ライブラリとしての正しさよりもエンドユーザーの使いやすさを重視する CLI 専用ツールに限ります。

並行処理のパターン

アンチパターン: 大規模な固定ワーカープール。Go のスケジューラーは非常に効率的なので、他の言語で OS スレッドのようにワーカーのプールを手動で管理する必要はありません。

1. 通信によってメモリーを共有する

共有データをチャネルで渡せる場合は、ミューテックスでそのデータを保護しないでください。チャネルは実行を調整し、ミューテックスは実行を直列化します。

2. 並行処理数に上限を設ける

並行処理数を制限するには、SetLimit を指定した errgroup を使います。これがあるのに、セマフォ用チャネルや固定ワーカープールを独自に実装しないでください。

func FetchAll(ctx context.Context, urls []string, maxConcurrent int) error {
    g, ctx := errgroup.WithContext(ctx)
    g.SetLimit(maxConcurrent)

    for _, url := range urls {
        g.Go(func() error {
            return fetch(ctx, url)
        })
    }

    return g.Wait()
}

Go 1.22 以降、ループ変数は反復ごとに個別の変数になります。古い url := url のキャプチャ行は絶対に出力しないでください。

エラー伝播が不要な場合は、sync.WaitGroup.Go(Go 1.25)を使うと、Add/Done の定型コードを省けます。

var wg sync.WaitGroup
for _, url := range urls {
    wg.Go(func() { process(url) })
}
wg.Wait()
3. 終了方法がわからないゴルーチンを開始しない

すべての go func() には明確な終了条件が必要です。通常は context.Context またはクローズされたチャネルで制御します。

Go 1.27 以降、このルールは単なる目標ではなく、測定可能になりました。goroutineleak プロファイルが runtime/pprof で一般提供され、/debug/pprof/goroutineleak エンドポイントからも利用できます。

import (
    "os"
    "runtime/pprof"
)

if p := pprof.Lookup("goroutineleak"); p != nil {
    _ = p.WriteTo(os.Stderr, 1)
}

リークしたゴルーチンとは、並行処理プリミティブ(チャネル、sync.Mutex、sync.Cond など)でブロックされ、ブロックが解除される可能性がないゴルーチンです。ランタイムはガベージコレクターによる到達可能性を使ってこれを証明します。この手法は到達可能性に基づくため、グローバル変数や実行可能なゴルーチンのローカル変数を介して到達できるプリミティブでブロックすることによるリークは検出できません。そのため、問題のないプロファイルは証拠ではありますが、証明ではありません。Go 1.26 では、このプロファイルは GOEXPERIMENT=goroutineleakprofile で有効にする実験的機能でした。この実験機能は Go 1.27 で削除されました。

設定と構造体の設計

複雑な初期化には関数オプションを使う

構造体に多数の任意の設定パラメーターがある場合は、大規模なコンストラクターを避けてください。関数オプションパターンを使います。

type Server struct {
    addr    string
    timeout time.Duration
}

type Option func(*Server)

func WithTimeout(d time.Duration) Option {
    return func(s *Server) { s.timeout = d }
}

func NewServer(addr string, opts ...Option) *Server {
    s := &Server{
        addr:    addr,
        timeout: 30 * time.Second, // Sane default
    }
    for _, opt := range opts {
        opt(s)
    }
    return s
}
new(expr) を使った任意のポインターフィールド(Go 1.26)

Go では、ポインターフィールドによって「存在しない」状態と「存在するがゼロ値」の状態を区別できます。Go 1.26 以降、new は式を受け取るため、一時変数が不要になります。

type Person struct {
    Name string
    Age  *int // nil means "not recorded"; a pointer separates absent from 0
}

// Old
age := yearsSince(born)
p := Person{Name: name, Age: &age}

// New (Go 1.26) — the canonical use of new(expr)
p := Person{Name: name, Age: new(yearsSince(born))}

Go 1.26 より前のバージョンでは、v := expr; &v の形式を使ってください。

エラー処理

1. エラーは値です

エラーは捕捉すべき例外ではなく、処理すべき値です。明示的に確認してください。

2. スタックトレースではなく、コンテキストを付与する

エラーを返すときは、何をしようとしていたかを示すコンテキストを追加してください。

// Idiomatic
data, err := os.ReadFile(path)
if err != nil {
    return fmt.Errorf("loading config file %s: %w", path, err)
}
3. errors.As ではなく errors.AsType[T] を使う(Go 1.26)

Go 1.26 以降を最低バージョンとするモジュールでは、errors.AsType[T](err) を使ってください。ジェネリックで型安全かつ高速であり、事前にターゲットを宣言したり、ポインターへのポインターを渡したりする必要がありません。

// Classic errors.As
var pathErr *fs.PathError
if errors.As(err, &pathErr) {
    // use pathErr
}

// Go 1.26 — the value falls straight out of the call
if pathErr, ok := errors.AsType[*fs.PathError](err); ok {
    // use pathErr
}

Go 1.26 より前のバージョンでは、&target を使う errors.As が引き続き必須の形式です。これは正しく、すべてのバージョンでサポートされています。

テストのパターン

アンチパターン: Ginkgo のような重量級の BDD フレームワークや、複雑なモック生成ツールに依存すること。Go のテストは、Go のプログラミングだけで行うべきです。

テスト可能かどうかは、配置によって決まります。 package main はインポートできないため、そこにあるコードを実行してテストするには、バイナリー全体をビルドするしかありません。サーバーを構築しないとテストできないドメインには、独自のパッケージが必要です。これが、パッケージ構成で分割の契機を設ける実務上の理由です。

1. テーブル駆動テスト

Go の単体テストにおける絶対的な標準です。入力と期待する出力を含む構造体のスライスを反復処理し、t.Run() を使います。

func TestParseConfig(t *testing.T) {
    tests := []struct {
        name    string
        input   string
        wantErr bool
    }{
        {"valid config", "port=8080", false},
        {"invalid format", "port=abc", true},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            _, err := ParseConfig(tt.input)
            if (err != nil) != tt.wantErr {
                t.Fatalf("ParseConfig() error = %v, wantErr %v", err, tt.wantErr)
            }
        })
    }
}
2. t.Helper() を使った意味のあるヘルパー

繰り返し使うアサーションロジックを切り出す場合は、必ず t.Helper() を呼び出してください。失敗時にヘルパー関数の行ではなく、実際のテストケースが示されます。

3. 重いモックよりフェイクとスタブ

Go の暗黙的なインターフェースを活用して、シンプルな手書きのフェイクを作成してください。これにより、テストの依存関係を軽量に保ち、テストロジックをわかりやすくできます。

4. ゴールデンファイルと testdata ディレクトリ

複雑な入力を必要とするテストや、大きな出力を生成するテストには、testdata という名前のディレクトリを使ってください。go test ツールは、これらのディレクトリを明示的に無視します。

5. ファイルシステムの抽象化(Afero パターン)

ビジネスロジックの深い階層で os パッケージの呼び出しをハードコードしないでください。テストをディスクに触れずにメモリー上で実行できるよう、ファイルシステムのインターフェースを受け取るようにします。業界標準は github.com/spf13/afero です。

import "github.com/spf13/afero"

type FileProcessor struct {
    fs afero.Fs
}

func NewFileProcessor(fs afero.Fs) *FileProcessor {
    return &FileProcessor{fs: fs}
}

テストでは afero.NewMemMapFs() を注入して、ディスク I/O を完全になくし、不安定で遅いテストを防ぎます。

6. DeepEqual ではなく cmp

複雑な構造体やマップを比較する場合は、厳密な reflect.DeepEqual ではなく、詳細で読みやすい差分を表示できる github.com/google/go-cmp/cmp を使ってください。

7. testing の最新機能(Go 1.24以降)
// Test-scoped context — canceled automatically when the test ends.
// Use instead of context.Background() in tests.
func TestFetch(t *testing.T) {
    ctx := t.Context()
    ...
}

// t.Chdir(dir) changes working directory for the test and restores it after.

ベンチマークでは必ず b.Loop() を使います。 B.Loop は Go 1.24 で導入され、ベンチマークで必須の形式です。Go 1.26 以降はループ本体のインライン化を妨げなくなったため、b.N からの移行は常に安全です。

func BenchmarkParse(b *testing.B) {
    for b.Loop() {
        Parse(input)
    }
}

for b.Loop() の本体内で代入された引数、結果、変数は引き続き生存した状態に保たれるため、コンパイラーがベンチマーク対象の呼び出しを最適化で取り除くことはありません。ただし、ループ条件は必ず b.Loop() と正確に記述する必要があります。for i := 0; i < b.N; i++ の形式は絶対に出力せず、両方の形式を混在させないでください。

テストの出力ファイルには t.ArtifactDir() を使います(Go 1.26)。 T.ArtifactDir、B.ArtifactDir、F.ArtifactDir は、テストが生成するファイル用のテストごとのディレクトリを返します。go test -artifacts を指定すると、このディレクトリは -outputdir(または現在のディレクトリ)の下に置かれます。フラグを指定しない場合は、テスト終了時に削除される一時ディレクトリになるため、そこに書き込んでもツリーが汚染されることはありません。

func TestRender(t *testing.T) {
    out := filepath.Join(t.ArtifactDir(), "render.png")
    if err := render(out); err != nil {
        t.Fatal(err)
    }
}

テスト出力のために独自の os.MkdirTemp によるクリーンアップを実装しないでください。また、testdata/ 内のフィクスチャの隣にアーティファクトを書き込まないでください。

決定論的な暗号学的乱数:cryptotest.SetGlobalRandom(Go 1.26)。 テストの実行中、グローバルな決定論的暗号学的乱数ソースにシードを設定します。これにより、crypto/rand と crypto/... パッケージ内の暗黙的な乱数生成に影響します。

func TestKeyGeneration(t *testing.T) {
    cryptotest.SetGlobalRandom(t, 42) // fixed seed for this test
    ...
}

プロセス全体の状態を変更するため、並列テストや、並列の先祖を持つテストでは使用できません。GODEBUG=cryptocustomrand=1 を設定すると、一時的に以前の動作に戻ります。

8. 並行コードに対する testing/synctest(Go 1.25)

偽のクロックを使って、ゴルーチンやタイムアウトを決定論的にテストできます。テストで time.Sleep を使ったり、不安定なタイミングの前提に頼ったりする必要はありません。

func TestTimeout(t *testing.T) {
    synctest.Test(t, func(t *testing.T) {
        ctx, cancel := context.WithTimeout(t.Context(), time.Second)
        defer cancel()
        time.Sleep(2 * time.Second) // instant — fake clock advances when all goroutines block
        if ctx.Err() == nil {
            t.Fatal("expected timeout")
        }
    })
}

テストで「ゴルーチンを待つ」ために time.Sleep(100 * time.Millisecond) を絶対に書かないでください。synctest、チャネル、または明示的な同期を使ってください。

Go 1.27 以降では、synctest.Sleep は time.Sleep と synctest.Wait を組み合わせたものです。バブル内の偽のクロックが指定した時間だけ進み、さらにバブル内のほかのすべてのゴルーチンが確実にブロックされるまで待機します。

synctest.Test(t, func(t *testing.T) {
    go worker(t) // system under test
    synctest.Sleep(time.Second) // advances the clock, then waits for it to settle
    // assertions see a quiescent system
})

バブル内では、単独の time.Sleep よりもこちらを優先してください。テストとテスト対象のコードが同じ時間だけスリープする場合、通常の time.Sleep では起床順序が予測できません。

Go 1.27 ではさらに、httptest.NewTestServer(t, handler) を使うと、ポートも実際のソケットも使わず、メモリ内の偽ネットワーク上で http.Server が実行されます。これにより、synctest バブル内でも HTTP テストを実行できます。

srv := httptest.NewTestServer(t, mux)
defer srv.Close()
resp, err := srv.Client().Get("http://example.com/") // routed to srv, address ignored

srv.Client() が返すクライアントは、宛先ホストに関係なく HTTP および HTTPS リクエストをそのサーバーに送るため、テストで srv.URL をベース URL として使う必要はありません。Go 1.27 より前の httptest.NewServer は、実際の localhost ポートにバインドするため、synctest バブル内では使用できません。

ジェネリクス(Go 1.18以降)

ジェネリクスはアルゴリズムの重複をなくすためのものであり、型階層を作るためのものではありません。継承やポリモーフィズムの観点でジェネリクスを考えているなら、そこで止めてください。Java を書いていることになります。

ジェネリクスを使う場合

同じアルゴリズムを複数の具体的な型で動作させる必要がある場合は、ジェネリクスを使います。

// Good: generic algorithm, concrete types as inputs
func Map[S, T any](slice []S, f func(S) T) []T {
    result := make([]T, len(slice))
    for i, v := range slice {
        result[i] = f(v)
    }
    return result
}

// Good: constraint expresses a meaningful requirement
func Min[T cmp.Ordered](a, b T) T {
    if a < b {
        return a
    }
    return b
}
ジェネリクスを使わない場合
// Bad: generic interface for polymorphism — this is Java
type Repository[T any] interface {
    Find(id string) (T, error)
    Save(entity T) error
}

// Good: a concrete interface for what you actually need
type UserStore interface {
    FindUser(id string) (*User, error)
    SaveUser(u *User) error
}
  • ジェネリックな基底型、サービス、リポジトリは作らないでください。
  • 「まだ型が分からない」という意味で、制約に any を使わないでください。これは設計上の問題を示しています。
  • マップのキーや等値比較が必要な場合は、comparable を使ってください。
  • <、>、<=、>= が必要な場合は、cmp.Ordered を使ってください。
  • まずは具体的な実装から始めてください。同じロジックが3つ以上の型で繰り返されている場合に限り、ジェネリックにしてください。
  • ジェネリック型エイリアスは Go 1.24 以降、完全にサポートされています。

標準ライブラリ:新しいパッケージを使う

LLM は、Go 1.21 以降の標準ライブラリにすでに含まれているサードパーティー製ユーティリティを頻繁に提案したり、手動のヘルパー関数を書いたりします。まず標準ライブラリを確認してください。

slices パッケージ(Go 1.21)
import "slices"

// Searching and testing
slices.Contains(s, "value")
slices.Index(s, "value")           // returns -1 if not found
slices.ContainsFunc(s, func(v string) bool { return v == "x" })

// Sorting
slices.Sort(s)                     // sorts in place, works on any ordered type
slices.SortFunc(s, func(a, b T) int { return cmp.Compare(a.Name, b.Name) })
slices.IsSorted(s)

// Manipulation
slices.Reverse(s)
slices.Compact(s)                  // removes consecutive duplicates
slices.Delete(s, i, j)            // removes elements [i, j)
slices.Clone(s)                   // shallow copy
slices.Concat(s1, s2, s3)         // concatenate multiple slices (1.22)

// Iterator bridging (1.23)
slices.Collect(it)                // iterator -> slice
slices.Sorted(it)                 // iterator -> sorted slice
slices.Values(s)                  // slice -> iterator

slices.Sort(s) が使える場合に、sort.Slice(s, func(i, j int) bool { return s[i] < s[j] }) を絶対に書かないでください。

maps パッケージ(Go 1.21;イテレーターは 1.23)
import "maps"

maps.Keys(m)        // iterator over keys — collect with slices.Collect / slices.Sorted
maps.Values(m)      // iterator over values
maps.Clone(m)       // shallow copy
maps.Copy(dst, src) // copies all entries from src into dst
maps.DeleteFunc(m, func(k K, v V) bool { ... })  // delete entries matching predicate
maps.Equal(m1, m2)  // reports whether two maps are equal

// Common idiom: sorted keys in one line
keys := slices.Sorted(maps.Keys(m))
cmp パッケージ(Go 1.21)
import "cmp"

cmp.Compare(a, b)    // returns -1, 0, or 1; works on any cmp.Ordered type
cmp.Or(a, b, c)      // returns first non-zero value — replaces ternary workarounds (Go 1.22)
min(a, b)            // built-in since Go 1.21
max(a, b)            // built-in since Go 1.21

cmp.Or は、デフォルト値を設定するパターンで特に便利です。

// Instead of: if cfg.Timeout == 0 { cfg.Timeout = 30 * time.Second }
cfg.Timeout = cmp.Or(cfg.Timeout, 30*time.Second)
errors.Join(Go 1.20)
// Combine multiple errors — no third-party library needed
err := errors.Join(err1, err2, err3)

// Works correctly with errors.Is and errors.As
if errors.Is(err, ErrNotFound) { ... }

fmt.Errorf("%w; %w", err1, err2) や multierr パッケージを使う代わりに、こちらを使います。

イテレーター:iter と Range-over-Func(Go 1.23)

スライスを割り当てずに API からシーケンスを公開する標準的な方法です。iter.Seq[T] または iter.Seq2[K, V] を返し、呼び出し側は通常の range を使います。

func (idx *Index) All() iter.Seq[*Person] {
    return func(yield func(*Person) bool) {
        for _, p := range idx.people {
            if !yield(p) {
                return
            }
        }
    }
}

// Caller — just a normal range loop
for p := range idx.All() { ... }

大規模なシーケンスや遅延生成されるシーケンスには、スライスではなくイテレーターを返すのがおすすめです。独自の Next()/HasNext() イテレーター型を作るのはやめましょう。それは Java のやり方です。

math/rand/v2(Go 1.22)

必ず math/rand/v2 をインポートし、旧来の math/rand は使わないでください。自動シードに対応し、API もすっきりしています。

import "math/rand/v2"

rand.IntN(100)            // was rand.Intn — note the capital N
rand.N(10 * time.Second)  // generic: random value in [0, n) for any integer type

ジェネリックな N は Go 1.27 からメソッドとしても使えるため、シード付きソースではパッケージレベルのジェネリックが不要になります。

r := rand.New(rand.NewPCG(seed1, seed2))
n := r.N(100) // Go 1.27 — in [0, 100), type inferred as int

Go 1.27 未満では、デフォルトのソースにはパッケージレベルの rand.N(100) を使うか、int でよければ r.IntN(100) を使います。

encoding/json:omitzero(Go 1.24)

omitzero は、time.Time{} やゼロ値の構造体など、omitempty では正しく処理できなかったものを含め、あらゆるゼロ値を省略します。

type Event struct {
    Name      string    `json:"name"`
    StartedAt time.Time `json:"started_at,omitzero"` // omitempty would emit "0001-01-01T..."
}

Go 1.27 では、encoding/json は encoding/json/v2 の実装をバックエンドとして使います。デフォルトの動作はより厳格になり、JSON 文字列内の不正な UTF-8 やオブジェクト内の重複した名前を拒否します。v1 のマーシャリングとアンマーシャリングの動作は維持されますが、エラーメッセージの文面は異なる場合があります。設定のデコードへの影響については、cobra-viper スキルで説明しています。

uuid パッケージ(Go 1.27)

標準ライブラリで UUID の生成と解析ができるようになりました。Go 1.27 を要求できるモジュールでは、反射的に github.com/google/uuid を推奨する必要はありません。

import "uuid"

u := uuid.New()      // algorithm suitable for most purposes
v7 := uuid.NewV7()   // time-ordered; good for database keys
id, err := uuid.Parse(s)

MustParse は入力が不正な場合にパニックを起こすため、ユーザー入力を受け取る箇所では Parse を使います。ランダムな要素は、暗号学的に安全なジェネレーターから取得されます。Go 1.27 未満では、github.com/google/uuid を使い続けてください。

strings.CutLast / bytes.CutLast(Go 1.27)

区切り文字の最後の出現箇所で分割し、LastIndex とスライス操作を組み合わせる慣用句を置き換えます。

// Old
i := strings.LastIndex(path, "/")
if i < 0 {
    return path, "", false
}
return path[:i], path[i+1:], true

// New (Go 1.27)
before, after, found := strings.CutLast(path, "/")

どちらも元のスライスの一部を返すためコピーは発生せず、found で見つかったかどうかを示します。Go 1.27 未満では、LastIndex を使う方法が代替手段です。

log/slog.NewMultiHandler(Go 1.26)

ログのラッパーを書かずに、レコードを複数のハンドラーに振り分けられます。たとえば、テキストを stderr に、JSON をファイルに出力できます。

handler := slog.NewMultiHandler(
    slog.NewTextHandler(os.Stderr, nil),
    slog.NewJSONHandler(file, nil),
)
logger := slog.New(handler)

MultiHandler.Enabled は、いずれかのハンドラーが有効な場合に true を返します。Go 1.26 未満では、ハンドラーのスライスに転送する小さな slog.Handler を実装します。

net/url のクローン(Go 1.27)

URL.Clone と Values.Clone はディープコピーを返します。自分で構築していない URL やクエリマップを変更する前に使ってください。単純な構造体コピーでは、userinfo ポインターとクエリマップが共有されたままです。

u2 := u.Clone()      // deep copy of all URL fields
q2 := q.Clone()      // deep copy of Values
q2.Set("page", "2")  // q is untouched

Go 1.27 未満では手動でコピーします(url.Userinfo と Values マップはそれぞれ個別にコピーする必要がある点に注意してください)。

reflect のイテレーター(Go 1.26)

Type.Fields、Type.Methods、Type.Ins、Type.Outs、Value.Fields、Value.Methods はイテレーターを返すため、このスキルのほかの箇所で使われている Range-over-Func スタイルと組み合わせられます。

for field := range reflect.TypeOf(cfg).Fields() {
    fmt.Println(field.Name, field.Type)
}

Go 1.26 未満では、NumField() / Field(i) でインデックスを指定します。

並行処理:最新のパターン

sync/atomic の型付き値(Go 1.19)

関数ベースの API ではなく、型付きのアトミック値を使います。

// Old (still works but avoid for new code)
var count int64
atomic.AddInt64(&count, 1)
val := atomic.LoadInt64(&count)

// New — type-safe, no pointer arithmetic
var count atomic.Int64
count.Add(1)
val := count.Load()

// Other typed atomics
var flag  atomic.Bool
var ptr   atomic.Pointer[MyStruct]
var val32 atomic.Int32
var val64 atomic.Uint64
context.WithoutCancel(Go 1.21)

コンテキストの値を保持したままキャンセルを切り離したい場合(リクエスト終了後も実行を続けるバックグラウンドタスクなど)に使います。

// The background job should keep running even after the HTTP request context cancels
bgCtx := context.WithoutCancel(requestCtx)
go doBackgroundWork(bgCtx)
time チャネルは常にバッファなしです(Go 1.27)

time パッケージが作成するチャネル(タイマーおよびティッカー)は、GODEBUG の設定にかかわらず、常にバッファなし(同期型)になりました。Go 1.23 では、go 1.23 以降を宣言するモジュールで容量 0 のタイマーチャネルが既定の動作となり、asynctimerchan=1 で無効化できました。Go 1.27 ではこの設定が恒久的に削除されたため、以前の動作に戻す方法はありません。

タイマーおよびティッカーを使用する際の影響:

  • Stop と Reset により、呼び出し前に準備された値が呼び出し後に送信または受信されないことが保証されます。Reset の前後でバッファ付きチャネルを使っていた従来の処理は不要になり、タイマーの発火値が暗黙に破棄されることもありません。
  • タイマーまたはティッカーチャネルの len と cap は 0 です。そのため、受信が成功するかどうかを判断するために長さをポーリングしてはいけません。select と default を使った非ブロッキング受信を使用してください。
  • ティッカーは、受信側の処理が遅れた場合、間隔を調整するかティックを破棄して遅れを取り戻します。そのため、Ticker.C から速やかに受信するか、ティッカーを Stop してください。
コードへの影響がないランタイムの変更(Go 1.26 / 1.27)

追いかけたり、「修正」したりする必要がないよう、知っておくべき動作:

  • Green Tea GC(Go 1.26) — 既定で有効になり、GC のオーバーヘッドが 10~40%減少します。GOEXPERIMENT=nogreenteagc で無効にできます(Go 1.27 で削除される予定です)。
  • ヒープベースアドレスのランダム化(Go 1.26) — 64 ビットプラットフォームでは、割り当てられたオブジェクトが予測可能なアドレスに配置されなくなりました。GOEXPERIMENT=norandomizedheapbase64 で無効にできます。
  • サイズに特化した小規模割り当て(Go 1.27) — 80 バイト未満の割り当てのコストが最大 30%低下します。その代わり、バイナリサイズは約 60 KB 増加します。GOEXPERIMENT=nosizespecializedmalloc で無効にできます(Go 1.28 で削除される予定です)。

HTTP:改良された標準ライブラリのルーターを使う(Go 1.22)

LLM は、単純なものを超えるルーティングでは、決まって gorilla/mux や chi を推奨します。Go 1.22 以降、標準の net/http ServeMux は、メソッドおよびパスパラメーターによるルーティングをネイティブに処理します。

mux := http.NewServeMux()

// Method-scoped routes
mux.HandleFunc("GET /users", listUsers)
mux.HandleFunc("POST /users", createUser)

// Path parameters — accessed via r.PathValue
mux.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")
    // ...
})

// Wildcard
mux.HandleFunc("GET /files/{path...}", serveFile)

名前付きルートの生成や正規表現による制約が必要な場合に限り、chi または gorilla/mux を使用してください。ミドルウェアはフレームワークを使う理由にはなりません(以下を参照)。メソッドとパスのみのルーティングであれば、標準ライブラリで十分です。

本番サーバー:必ずタイムアウトを設定する

http.ListenAndServe(addr, mux) にはタイムアウトが設定されていません。そのため、遅いクライアントが 1 つあるだけで接続が永久に開いたままになることがあります(スローリス攻撃)。LLM が生成するサーバーでは、タイムアウトがほとんど設定されません。本番用サーバーをタイムアウトなしで出力してはいけません。

srv := &http.Server{
    Addr:              ":8080",
    Handler:           mux,
    ReadHeaderTimeout: 5 * time.Second,   // slow-loris protection
    ReadTimeout:       10 * time.Second,  // full request read
    WriteTimeout:      30 * time.Second,  // response write (covers handler time)
    IdleTimeout:       120 * time.Second, // keep-alive connections
}

WriteTimeout を超えるルート単位の制御には、http.TimeoutHandler またはハンドラー単位の context.WithTimeout を使用してください。外向きの呼び出しにも同じ注意が必要です。http.DefaultClient にもタイムアウトは設定されていないため、タイムアウトを設定したクライアントを作成してください。

グレースフルシャットダウン

本番サーバーにはすべて、新しい接続の受け付けを停止し、処理中のリクエストを完了させるためのシャットダウン処理が必要です。次のパターンを使用します。

func run(ctx context.Context) error {
    ctx, stop := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)
    defer stop()

    srv := &http.Server{ /* ... timeouts as above ... */ }

    errCh := make(chan error, 1)
    go func() { errCh <- srv.ListenAndServe() }()

    select {
    case err := <-errCh:
        return err // ListenAndServe failed at startup
    case <-ctx.Done():
        shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
        defer cancel()
        return srv.Shutdown(shutdownCtx) // drain in-flight, then exit
    }
}
  • Shutdown には独自の期限を持つ新しいコンテキストが必要です。シグナルコンテキストはすでにキャンセルされています。
  • 長時間接続(SSE、WebSocket)では r.Context() を監視する必要があります。監視しないと、ドレインの期限までシャットダウンが完了しません。
ミドルウェアは単なる関数です

フレームワークは必要ありません。ミドルウェアは func(http.Handler) http.Handler です。

func withRequestLog(logger *slog.Logger, next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        next.ServeHTTP(w, r)
        logger.Info("request", "method", r.Method, "path", r.URL.Path, "dur", time.Since(start))
    })
}

// Compose by wrapping — outermost runs first
handler := withRequestLog(logger, withAuth(mux))

関数の合成で実現できることに、ミドルウェアフレームワークをインポートしないでください。

構文:現在の Go を使う

LLM は古い構文を頻繁に生成します。現在の慣用的な書き方を把握しておきましょう。

整数に対する range(Go 1.22)
// Old
for i := 0; i < 10; i++ { ... }

// New
for i := range 10 { ... }
new(expr)(Go 1.26)

new は式を受け取り、その値を保持する新しい変数へのポインターを返します。これにより、v := expr; &v という書き方は不要になります。

// Old
v := yearsSince(born)
p := &v

// New (Go 1.26)
p := new(yearsSince(born))

代表的な使用例は、オプションのポインター型構造体フィールドです。上記の「Configuration and Struct Design」を参照してください。

ジェネリックメソッド(Go 1.27)

メソッド宣言では独自の型パラメーターを宣言できるため、ジェネリックなヘルパーをパッケージ全体ではなく型にスコープできます。math/rand/v2 には、既存のパッケージレベルのジェネリック関数 N に加えて、メソッド形式の (*Rand) N[Int intType](Int) Int が追加されます。

import "math/rand/v2"

r := rand.New(rand.NewPCG(seed1, seed2))
n := r.N(100) // method form, Go 1.27

次の2つの厳格な制限があります。

  • インターフェースのメソッドでは、型パラメーターを宣言できません。
  • インターフェースのメソッドをジェネリックメソッドで実装することはできません。

Go 1.27 より前では、ヘルパーをパッケージレベルのジェネリック関数に移してください。

自己参照型パラメーター制約(Go 1.26)

ジェネリック型は、自身の型パラメーターリストで自身を参照できるようになりました。

type Adder[A Adder[A]] interface {
    Add(A) A
}

Go 1.26 より前では、コンパイラーによって拒否されていました。

構造体リテラルのキー:任意のフィールドセレクター(Go 1.27)

構造体リテラルのキーには、トップレベルのフィールド名だけでなく、その構造体型に対する有効なフィールドセレクターを使用できます。そのため、プロモートされたフィールドを直接指定できます。

type Base struct{ ID int }

type User struct {
    Base
    Name string
}

u := User{ID: 1, Name: "steve"} // Go 1.27 — Base.ID named directly

Go 1.27 より前では、埋め込み値を明示的に初期化してください:User{Base: Base{ID: 1}, Name: "steve"}。

関数型推論の一般化(Go 1.27)

ジェネリック関数が、対応する関数型の値に代入される場合、またはその値に変換される場合は、どのコンテキストでも関数型推論が適用されます。

type Less func(a, b string) int

// Go 1.26 and earlier: "cannot use generic function cmp.Compare without instantiation"
var less Less = Less(cmp.Compare) // Go 1.27 infers T = string

Go 1.27 より前では、明示的にインスタンス化してください:cmp.Compare[string]。

ビルド制約
// Old (deprecated — do not generate)
// +build linux darwin

// Current
//go:build linux || darwin

Go 1.17 以降では、//go:build 形式が必須です。古い // +build 構文は絶対に出力しないでください。

interface{} の代わりに any
// Old
func Print(v interface{}) { ... }

// Current — `any` is a built-in alias for interface{} since Go 1.18
func Print(v any) { ... }
go.mod のツール依存関係(Go 1.24)

空のインポートを使った tools.go ファイルは絶対に生成しないでください。開発ツールは tool ディレクティブで管理してください。

go get -tool golang.org/x/tools/cmd/stringer
go tool stringer -type=Color   # run it

log/slog による構造化ログ(Go 1.21)

上記のスキルノートで基本的な設定を説明しています。LLM が特によく間違えるパターンは次のとおりです。

// Pass a logger via context for request-scoped logging
func HandleRequest(ctx context.Context, r *Request) {
    logger := slog.With("request_id", r.ID, "user_id", r.UserID)
    // All subsequent log calls carry these fields automatically
    logger.Info("handling request")
    process(ctx, logger, r)
}

// Group related fields
logger.With(slog.Group("http",
    slog.String("method", r.Method),
    slog.String("path", r.URL.Path),
    slog.Int("status", status),
))

// Log at the right level — LLMs over-use Info
logger.Debug("cache miss", "key", key)     // internal state, high volume
logger.Info("server started", "addr", addr) // lifecycle events
logger.Warn("retrying", "attempt", n)       // recoverable problems
logger.Error("request failed", "err", err)  // needs attention
  • 絶対に、main 以外でパッケージレベルの log または slog グローバルを使用しないでください。依存関係として *slog.Logger を渡してください。
  • 絶対に、エラーをログに記録してから返さないでください。境界でログに記録し、呼び出しスタックを通じてエラーを返してください。
  • フォールバックとして slog.Default() を使うのは、main 内か、ロガーが提供されないライブラリ内だけにしてください。

実験的パッケージ:本番環境では使用しない

これらの API は GOEXPERIMENT のビルド時フラグを使って提供され、その API は安定していないことが明示されています。プロトタイプを作成する場合や、将来のリリースに向けて準備する場合には使用できますが、本番コードや、他者が依存するライブラリには絶対に含めないでください。

  • simd(Go 1.27) — GOEXPERIMENT=simd を使って提供される、ポータブルでベクトルサイズに依存しない SIMD で、すべてのアーキテクチャで利用できます。ベクトルコードを一度記述すれば、コンパイラーがハードウェアの幅に合わせてマッピングします。
  • simd/archsimd(Go 1.26、Go 1.27 で拡張) — GOEXPERIMENT=simd を使って提供される、アーキテクチャ固有の SIMD です。Go 1.26 で amd64 向けに導入され、Go 1.27 では arm64 Neon と WebAssembly 128-bit SIMD に拡張されます。意図的にポータブルではありません。
  • runtime/secret(Go 1.26) — GOEXPERIMENT=runtimesecret を使って提供され、amd64 および arm64 の Linux のみで利用できます。Go 1.27 では、secret モード中に作成されたゴルーチンも secret モードで実行されます。

デバッグ:Go ツールチェーンに問題はありません

Go ツールは非常に信頼性が高く、バグの原因になることはほとんどありません。

デバッグ時に、go run、go build、go test、またはビルドキャッシュを疑って時間を無駄にしないでください。Go ツールチェーンは、その説明どおりに動作します。

  • go run は常にソースから再コンパイルします。古いキャッシュ済みバイナリは使用しません。
  • go build は決定的で正しく動作します。
  • go test は、実際にコンパイルされたテストバイナリを実行します。
  • ビルドキャッシュはソースの内容に基づいて管理されるため、ソースが変更されると自動的に無効になります。

コードを編集した後もエラーが続く場合、考えられる理由は次のいずれかです(可能性の高い順)。

  1. 編集によって、根本的なロジックエラーが修正されていません。
  2. 編集したファイル、関数、またはパッケージが間違っています。
  3. 同じバグがある別の呼び出し箇所があり、更新されていません。
  4. エラーは、編集中のコードとは別のコードパスから発生しています。

ツールのせいにするのではなく、次のことを行ってください。

  • エラーメッセージを注意深く読み直してください。Go のエラーメッセージは正確です。
  • 編集したファイルが実際にコンパイル対象になっていることを確認してください(go list -f '{{.GoFiles}}' .)。
  • 該当箇所に fmt.Println または t.Log を追加して、実行がそこまで到達していることを確認してください。
  • 変更した関数の呼び出し箇所がすべて更新されていることを確認してください。

まずコードに起因する可能性をすべて調べる前に、ビルドキャッシュの消去(go clean -cache)、Go ツールチェーンの再起動、その他のツールレベルの対処を提案しないでください。ツールは嘘をついていません。

コメント