go-generics について
Go 開発者が型パラメーターの使いどころと使い方を判断するためのスキルです。まず具体的なコードを書き、共通ロジックが必要な場合に一般化する方針を示します。
主な機能
- 01ジェネリクスとインターフェースの選択指針
- 02共用型と基底型による制約の構成
- 03型パラメーターの命名規則
- 04よくある問題と標準制約
活用シーン
- 01共通ロジックにジェネリクスが必要か判断
- 02再利用可能な Go 関数の制約設計
- 03不要な複雑さがないかジェネリックコードを確認
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ cxuu/ golang-skills --skill go-genericsインストール後、Claude Code で/go-genericsを入力して読み込まれたか確認します。
同梱ファイルreferences/CONSTRAINTS.md
詳細
Go のジェネリクスと型パラメーター
互換性:ジェネリクスには Go 1.18 以降が必要です。
リソースのルーティング
references/CONSTRAINTS.md- 制約を組み立てる場合、型セットを使用する場合、またはジェネリクスとインターフェースのどちらを選ぶか検討する場合に参照してください。
ジェネリクスを使う場面
まずは具体的な型で書きます。2 つ目の型が登場してから汎用化します。
ジェネリクスを使うのが望ましい場面
- 複数の型で同じロジックを共有する場合(ソート、フィルタリング、map/reduce)
- そうでなければ
anyや過剰な型スイッチに頼ることになる場合 - 再利用可能なデータ構造(並行安全なセット、順序付きマップ)を構築する場合
ジェネリクスを避ける場面
- 実際にインスタンス化する型が 1 つだけの場合
- インターフェースで共有動作をすでに適切にモデル化できている場合
- ジェネリックコードが、型固有の代替コードより読みにくい場合
「型を設計するのではなく、コードを書きましょう。」— Robert Griesemer と Ian Lance Taylor
判断の流れ
Do multiple types share identical logic?
├─ No → Use concrete types
├─ Yes → Do they share a useful interface?
│ ├─ Yes → Use an interface
│ └─ No → Use generics
悪い例:
// Premature generics: only ever called with int
func Sum[T constraints.Integer | constraints.Float](vals []T) T {
var total T
for _, v := range vals {
total += v
}
return total
}
良い例:
func SumInts(vals []int) int {
var total int
for _, v := range vals {
total += v
}
return total
}
型パラメーターの命名
| 名前 | 典型的な用途 |
|---|---|
T | 一般的な型パラメーター |
K | マップのキー型 |
V | マップの値型 |
E | 要素/アイテム型 |
複雑な制約では、短く説明的な名前を使用してもかまいません。
func Marshal[Opts encoding.MarshalOptions](v any, opts Opts) ([]byte, error)
型エイリアスと型定義
型エイリアス(type Old = new.Name)はまれです。パッケージの移行や段階的な API のリファクタリングに限って使用してください。
制約の組み合わせ
~(基底型)と |(ユニオン)を使って制約を組み合わせます。
type Numeric interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~float32 | ~float64
}
func Sum[T Numeric](vals []T) T {
var total T
for _, v := range vals {
total += v
}
return total
}
独自に作成する代わりに、cmp.Ordered などの標準的な制約には constraints パッケージまたは cmp パッケージ(Go 1.21 以降)を使用してください。
よくある落とし穴
標準ライブラリの型をラップしない
// Bad: generic wrapper adds complexity without value
type Set[T comparable] struct {
m map[T]struct{}
}
// Better: use map[T]struct{} directly when the usage is simple
seen := map[string]struct{}{}
ジェネリクスが複雑さに見合うのは、複数の呼び出し箇所にわたる重複をなくせる場合です。1 回しか使わないジェネリクスは、単なる間接化です。
インターフェースの実装を目的にジェネリクスを使わない
// Bad: T is only used to satisfy an interface — just use the interface
func Process[T io.Reader](r T) error { ... }
// Good: accept the interface directly
func Process(r io.Reader) error { ... }
制約を過剰に設定しない
// Bad: constraint is more restrictive than needed
func Contains[T interface{ ~int | ~string }](slice []T, target T) bool { ... }
// Good: comparable is sufficient
func Contains[T comparable](slice []T, target T) bool { ... }
クイックリファレンス
| トピック | 指針 |
|---|---|
| ジェネリクスを使う場面 | 複数の型で同じロジックを共有し、インターフェースでは不十分な場合に限る |
| 出発点 | まず具体的なコードを書き、後から汎用化する |
| 命名 | 大文字 1 文字(T、K、V、E) |
| 型エイリアス | 同じ型の別名。移行に限って使用する |
| 制約の組み合わせ | 基底型には ~、ユニオンには ` |
| よくある落とし穴 | 1 回しか使わないコードや、インターフェースで十分な場合にジェネリック化しない |
関連スキル
- インターフェースとジェネリクスの比較:ジェネリクスを使わずにインターフェースで共有動作をモデル化できるか判断する場合は、go-interfaces を参照してください
- 型宣言:新しい型や型エイリアスを定義する場合、または型定義と型エイリアスのどちらを選ぶか検討する場合は、go-declarations を参照してください
- ジェネリック API のドキュメント作成:ジェネリック関数のドキュメントコメントや実行可能な例を書く場合は、go-documentation を参照してください
- 型パラメーターの命名:型パラメーターや制約インターフェースの名前を選ぶ場合は、go-naming を参照してください
コメント
Markdown 対応