Communityコーディング&開発github.com

davidvella/go-style

読みやすく、それでいて高速なGoコードを記述、レビュー、リファクタリングします。関数を小さく保ち、循環的複雑度と認知的複雑度を低く抑え、パフォーマンスを巧妙なコードではなくAPI設計とデータレイアウトに委ね、ベンチマーク、アロケーション数、エスケープ分析で変更を証明します。

go-style とは?

このスキルは、Goのコードベースにおいて「読みやすく、かつ高速な」コードを書くためのガイドラインと実践方法を提供します。主な目的は、メンテナがコードを上から下へと読めるようにし、パフォーマンスはAPIの形状やデータ構造から生まれ、コード中のトリックに依存しないようにすることです。Dave Cheneyの「API Design as a Performance Primitive」の考え方を基盤とし、ホットパスとコールドパスを明確に分離し、API設計段階でパフォーマンスを考慮することを重視します。関数は小さくフラットに保ち、循環的複雑度10以下、認知的複雑度10以下を目指します。また、変更を行う際には必ずベンチマークによる測定とエスケープ分析の確認を求め、数値による証明を徹底します。対応プラットフォームはGo言語で記述されたあらゆるプロジェクト(CLIツール、サーバーサイドアプリケーション、ライブラリなど)です。このスキルは、Claude CodeやCursorなどのAIエージェントがGoのコードレビューやリファクタリングを行う際に適用され、コードの品質とパフォーマンスのバランスを保証します。

対応~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/davidvella/skills/tree/HEAD/skills/go-style

Installed? Explore more コーディング&開発 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

お気に入りのAIに質問する

このエージェントスキルを事前に読み込んだ状態で新しいチャットを開きます。

ドキュメント

Go style: 読みやすく高速に

目標は、メンテナが上から下へ読めるコードであり、パフォーマンスはコード全体に散らばったトリックではなく、APIとデータの形状から生まれることです。設計が適切であれば、読みやすさと速度が衝突することはほとんどありません。衝突した場合のみ、計測されたホットパス上で読みやすさを犠牲にし、その理由をコメントに記述します。

これはDave Cheneyの「API Design as a Performance Primitive」に従っています。ある点を超えると、パフォーマンスを後から追加することはできないため、設計段階でパフォーマンスを考慮し、それ以外はすべてシンプルに保ちます。

出典: Cheneyのトーク (https://dave.cheney.net/paste/gophercon-uk-2026.html)、コードはgithub.com/pkg/jsonおよびgithub.com/davecheney/bytereaderにあります。John Ousterhout、「Define errors out of existence」。

優先順位

  1. 正確性。 速度や簡潔さと引き換えに絶対にしない。
  2. APIの形状とbig-O。 メモリの所有権、何が返されるか、各呼び出しがどれだけの作業を行うか。これらが後からのどのようなチューニングよりもパフォーマンスを決定づける。
  3. ホットパス上のアロケーション。 アロケーションごとに、割り当てとガベージコレクタの2回のコストがかかる。コレクタは高速かもしれないが、「アロケーションしないことは常に高速である」。
  4. 入力単位あたりの作業量。 各バイトを1回だけ触り、呼び出しはバイト単位ではなく、トークンまたはレコード単位で行う。
  5. マイクロ最適化(手動インライン化、ビットトリック、境界チェックの回避)。プロファイルでホットと示されたパスのみで行い、かつ利益を証明するベンチマークがある場合のみ。

変更が上のレベルで正当化されない場合は、行わないでください。

ホットパスとコールドパスを分離する

どのコードがイベントやリクエストごとに実行されるか(ホット)と、セットアップ、ビルド、設定、または更新時に実行されるか(コールド)を決定します。

  • コールドパス: 最もシンプルなコードを書く。共有する代わりにクローンし、マップやfmtを使用し、自由にアロケーションする。ホットパスのイディオムを持ち込まないでください。読みやすさを犠牲にし、何も得られません。
  • ホットパス: 再利用可能なバッファ、世代カウンター、フラット配列などのテクニックを使用してもよい。各テクニックに名前(小さな型やヘルパー)と、それが存在する理由を説明するコメントを付け、読者がリバースエンジニアリングする必要がないようにします。

パフォーマンスのためにAPIを設計する

APIによって、呼び出し元が節約できる上限が決まります。APIがすべての結果をヒープに強制する場合、「ガベージコレクタのベンチマークを取っている」ことになります。高速パスが自然なものになるように設計してください。「異なるAPIを受け入れる用意があれば」、2〜10倍の利得が得られることがよくあります。

  • エスケープしなければならない値を返さない。 プリミティブをinterface{}/any、新しい文字列、マップ、またはアイテムごとの新しい構造体へのポインタを介して返すと、呼び出しごとにアロケーションが強制されます(encoding/json.Decoder.Tokenは古典的な「ガベージファクトリー」です)。具体的な型、または[]byteのようなビューを返し、呼び出し元が必要なものだけを変換できるようにします。
  • コピーではなくビューを返す。 呼び出し元が読み取りのみを必要とする場合。ビューが有効な期間を文書化します(「次の呼び出しまで有効」、bufio.Scanner.Bytesのように)。生の入力のビュー(トークンのバイト、その最初のバイトが種類を示す)は、呼び出し元が無視するかもしれないデコードされた値よりもコストが低いです。
  • 呼び出し元が再利用可能なメモリを所有できるようにする。 スクラッチやバッファ引数を受け入れるか、呼び出し元が保持する構造体内に保持し、呼び出しごとにアロケーションしないようにします。呼び出し元が初期バッファを提供できるようにし、スタック上やプールから取得できるようにします: var buf [8 << 10]byte; d := NewDecoderBuffer(r, buf[:])。
  • バッファ管理を少数のメソッドの背後に隠す。 io.Readerは呼び出し元管理のバッファにコピーし、補充をすべての呼び出し元に任せます。小さなウィンドウAPIはデータをその場に保持します: 参照にはWindow() []byte、消費にはRelease(n)、さらに読み取るにはExtend() int。「ウィンドウはReleaseまたはExtendによって無効化される」ことをドキュメントコメントに記述します。
  • エラーをホットループの外に定義する。 ブール値または空の結果を返し、エラーを後で報告するイテレーションを使用します: for s.Next() { ... } その後 s.Err()。スローパスでセンチネル結果を使用し(Extendは0を返し、その後呼び出し元がエラーを問い合わせる)、内部ループにエラーブランチがないようにします。io.EOFを他の終端条件と同様に扱います。
  • 入力単位あたりの作業量をカウントする。 各バイトを1回処理し、バイト単位ではなくトークンまたはレコード単位で関数呼び出しを行い、中間コピーを排除します。バイト単位からトークン単位の呼び出しへの移行は、アロケーションの除去に次いで2番目に大きな効果があることがよくあります。
  • APIを階層化する。 低レベルでアロケーションのないビルディングブロック(生のトークン、ビュー、スクラッチを所有するイテレータ)を提供し、その上に便利なAPIを構築します。便利でアロケーションを行うAPIのみを提供しないでください。
  • 所有権とライフタイムをドキュメントコメントに記述する: 何が借用され、何が保持され、何が同時使用に対して安全か、ゼロ値が何を意味するか。
  • ゼロ値を有用にする。 それが自然な場合。
  • 変更可能な共有状態のロックよりも、不変の共有値と新しいスナップショットの公開を優先する。 読み取りが支配的な場合。

APIを測定し、実装だけでなく、置き換えるAPIと比較してallocs/opとスループットを比較します。references/examples.mdには、これらの形状が並べて示されています。

ホットループのチューニングは最後に

APIとbig-Oが適切であり、プロファイルがループがホットであることを示した後でのみ:

  • 状態に対するswitchは、比較の連鎖にコンパイルされます。funcフィールド(d.state = (*Decoder).stateValue)を介して選択される、状態ごとの小さなメソッドにより、各状態を読みやすく保ち、そのディスパッチを除去できます。
  • 値を使用する関数にチェックを移動することで、コンパイラが境界チェックを不要と証明できるようにし、エントリポイントをインライン化予算(80;呼び出しには約52のコストがかかる)内に収めます。go build -gcflags=-m=2で確認します。
  • 呼び出しを避けるために2つの関数をマージすると、トークン単位のパスで5〜15%の利得が得られることがありますが、ネストが深くなります。ベンチマークがある場合のみ行い、その理由をコメントに記述します。

Cheneyが述べるように、「APIのbig-O効果に取り組まずにこれらのトリックに手を出してはいけない。それは針を動かさない。」

関数を小さくフラットに保つ

各関数が1つのことを行い、最大1〜2つのブランチを持つことを目指します。ガイドとして、循環的複雑度を10以下、認知的複雑度を10以下に保ちます(gocyclo、gocognit)。各ケースを1つの呼び出しに委譲するフラットな型スイッチは、それを超えても問題ありません。ネストは禁止です。

  • 複数条件に名前を付ける。 if len(a)*len(b) > maxN || size(a)*len(b)+size(b)*len(a) > maxM を if !fitsProduct(a, b) に置き換えます。名前が意図を文書化し、関数が説明を担うことができます。
  • ディスパッチしてから委譲する。 ノードやメッセージの種類に対するスイッチは、各ケースのロジックを含むのではなく、ケースごとに1つのメソッドを呼び出すべきです。
  • 早期リターン。 ガード節を最初に、メインパスはインデントなしで。
  • ケースを繰り返さない。 2つのブランチがオペランドの順序のみ異なる場合、1つのヘルパーを書き、それを2回呼び出します。
  • 混合関心事を分割する。 「見つける」、「新しい値を構築する」、「整理する」は3つの関数であり、1つではありません。
  • ブール型フラグパラメータを避ける。 それが最も明確なオプションである場合、意味のある名前を付け、呼び出しサイトで説明します。
  • ルックアップテーブルを使用する(map[string]Kind、[256]bool)。文字列やバイト比較の連鎖の代わりに。

読者とCPUのためにデータを整形する

  • 並列スライスを構造体のスライスにグループ化する。 同じIDでインデックス付けされた4つのスライスは、1つの構造体です。読みやすくなり、通常は局所性が向上します。
  • 繰り返されるメカニズムに独自の小さな型を与える。 同じカウンターラップ、ビットマップ、バッファロジックが2回出現する場合、それは2つまたは3つのメソッドを持つ型です。
  • ビット詰めよりも明示的なフィールドを優先する。 詰め込みが測定上重要でない限り。シフト演算を伴うフラグのuint64よりも、[]segment{name, each}の方が優れています。
  • 表現を少なく保つ。 状態や種類が少なければ、重複するスイッチも少なくなります。測定可能なコストがない場合、特殊なケースを一般的なケースに統合します。

パフォーマンスの主張を証明する

測定せずに、変更が高速、低速、またはアロケーションなしであると主張しないでください。パフォーマンスに敏感なコードを変更する前に:

  1. 関連するベンチマークのbenchstatベースラインを保存します。
  2. 変更を行い、比較します。有意な差異を数値で報告します。
  3. go build -gcflags=-mの出力をベースと比較し、新しいヒープエスケープを確認します。
  4. ゼロアロケーションを約束するパスに対して、testing.AllocsPerRunテストを維持または追加します。
  5. プロファイルを作成してから、リグレッションの原因を推測します。

簡略化によりホットパス時間が測定可能なほど失われ、それをきれいに回復できない場合、停止し、トレードオフを報告します。静かに複雑さを追加したり、静かにリグレッションを受け入れたりしないでください。

正確なコマンド、複雑度の測定方法、および一般的にリグレッションを引き起こす落とし穴については、references/verify.mdを参照してください。スタイルを示すリファクタリングの前後については、references/examples.mdを参照してください。

Goの慣例に従う

  • gofmt、go vet、およびプロジェクトのリンターが合格する必要があります。コードを修正し、リンタールールを弱めたり除外したりしないでください。名付けたルールと理由がある場合のみ、1行を抑制します。
  • 短いレシーバー名、型のメソッド間で一貫性を保ちます。理由なく1つの型でポインタレシーバーと値レシーバーを混在させないでください。
  • ドキュメントコメントは識別子の名前で始め、契約を述べます。
  • 何ではなく、なぜをコメントします。周囲のコメント密度に合わせます。
  • 標準ライブラリ(slices、maps、strings、math/bits)を手書きのヘルパーより優先します。
  • 生成されたコードを決して手動編集しないでください。

レビューまたはリファクタリング時

  1. 何かに触れる前に、ホットパスとそのベンチマークを特定します。
  2. 複雑度を測定して最悪の関数を見つけ、それらを最初に修正します。
  3. 小さなステップでリファクタリングし、各ステップの後に関連テストを実行します。
  4. 動作を同一に保つ: 同じ結果、同じ予算とフォールバック、同じアロケーション保証。動作の変更は、別個の明示的な決定としてのみ行います。
  5. 何が変わったか、変更前後の複雑度とベンチマーク数値、および検証されていないものを報告します。

関連スキル