Goジェネリックス:すべてを知る必要のあること
Daniel Hayes
Full-Stack Engineer · Leapcell

Genericsとは
ジェネリックプログラミングは、プログラミング言語のスタイルまたはパラダイムです。ジェネリックを使用すると、プログラマーは、後で指定される型を使用して、厳密に型付けされたプログラミング言語でコードを記述できます。これらの型は、インスタンス化中にパラメーターとして提供されます。
ジェネリックを使用すると、型ごとに同じロジックを繰り返すことなく、複数の型に適用できるコードを記述できます。これにより、コードの再利用性、柔軟性、および型の安全性が向上します。
Goでは、ジェネリックは型パラメーターを介して実装されます。型パラメーターは、プレースホルダーとして使用される特別な種類のパラメーターであり、任意の型にすることができます。これらは、関数、メソッド、および型の定義で使用され、具体的な呼び出し中に特定の型に置き換えられます。
Generics以前
2つのint
パラメーターを受け取り、2つのうち小さい方を返す関数を実装するという要件を考えてみましょう。これは非常に単純な要件であり、次のコードを簡単に記述できます。
func Min(a, b int) int { if a < b { return a } return b }
これは素晴らしいように見えますが、関数には制限があります。パラメーターはint
型のみにすることができます。要件が拡張され、2つのfloat64
値の比較をサポートし、小さい方を返す必要がある場合は、次のように記述できます。
func Min(a, b int) int { if a < b { return a } return b } func MinFloat64(a, b float64) float64 { if a < b { return a } return b }
要件が拡張されるとすぐに変更を加えなければならず、常に反復的な作業を行っていることに気付くかもしれません。ジェネリックはまさにこの問題を解決するものです。
import "golang.org/x/exp/constraints" func Min[T constraints.Ordered](x, y T) T { if x < y { return x } return y }
Genericsの基本的な構文
// 関数の定義 func F[T any](p T){...} // 型の定義 type M[T any] []T // Constraintは、any、comparableなどの特定の型の制約を表します func F[T Constraint](p T){..} // 「〜」記号は、基になる型の制約を表すために使用されます type E interface { ~string } // 複数の型を指定する type UnionElem interface { int | int8 | int32 | int64 }
〜記号
Goジェネリックでは、〜
記号は、基になる型の制約を表すために使用されます。
たとえば、〜int
は、基になる型がint
である任意の型を受け入れることを意味します。カスタム型を含みます。基になる型がint
であるカスタム型MyInt
がある場合、この制約はMyInt
型を受け入れることができます。
type MyInt int type Ints[T int | int32] []T func main() { a := Ints[int]{1, 2} // 正しい b := Ints[MyInt]{1, 2} // コンパイルエラー println(a) println(b) }
MyInt does not satisfy int | int32 (possibly missing ~ for int in int | int32)compilerInvalidTypeArg
次のように修正するだけです。
type Ints[T ~int | ~int32] []T
型の制約
any
: 任意の型を受け入れますcomparable
:==
および!=
演算をサポートしますordered
:>
、<
などの比較演算子をサポートします
その他の型については、https://pkg.go.dev/golang.org/x/exp/constraintsを参照してください。
Genericsを使用するタイミング
Genericsを使用するタイミング
- 言語定義のコンテナ型に対する操作の場合: スライス、マップ、チャネルなど、言語によって定義された特殊なコンテナ型を操作するfunctionsを作成する場合、functionsコードが要素型について特定の仮定を立てない場合、型パラメーターを使用すると役立つ場合があります。たとえば、任意の型のマップからすべてのキーのスライスを返すfunctionsなどです。
- 汎用データ構造: リンクリストやバイナリツリーなどの汎用データ構造の場合、型パラメーターを使用すると、より汎用的なデータ構造を生成したり、データをより効率的に保存したり、型アサーションを回避したり、ビルド時に完全な型チェックを実行したりできます。
- ジェネリックメソッドの実装: 異なる型がいくつかの共通メソッドを実装する必要があり、これらの実装がまったく同じに見える場合、型パラメーターを使用すると役立つ場合があります。たとえば、任意のスライス型に対して
sort.Interface
を実装するジェネリック型。 - メソッドよりもfunctionsを優先する: 比較functionsのようなものが必要な場合は、メソッドではなくfunctionsを使用することを優先します。汎用データ型の場合、メソッドを必要とする制約を記述するよりも、functionsを使用することをお勧めします。
// SliceFnは、Tのスライスのsort.Interfaceを実装します。 type SliceFn[T any] struct { s []T less func(T, T) bool } func (s SliceFn[T]) Len() int { return len(s.s) } func (s SliceFn[T]) Swap(i, j int) { s.s[i], s.s[j] = s.s[j], s.s[i] } func (s SliceFn[T]) Less(i, j int) bool { return s.less(s.s[i], s.s[j]) }
// SortFnは、比較functionsを使用してsをインプレースでソートします。 func SortFn[T any](s []T, less func(T, T) bool) { sort.Sort(SliceFn[T]{s, less}) }
Genericsを使用しないタイミング
- インターフェース型を置き換えないでください: 特定の型の値に対してメソッドを呼び出すだけでよい場合は、型パラメーターではなくインターフェース型を使用する必要があります。たとえば、インターフェース型を使用するfunctionsを、型パラメーターを使用するfunctionsに変更しないでください。
- メソッドの実装が異なる場合は、型パラメーターを使用しないでください: メソッドの実装が型ごとに異なる場合は、型パラメーターを使用するのではなく、インターフェース型を使用して、異なるメソッドの実装を記述する必要があります。
- 必要に応じてリフレクションを使用する: メソッドを持たない型でさえサポートする必要がある操作があり、操作が型ごとに異なる場合は、リフレクションを使用します。たとえば、
encoding/json
パッケージはリフレクションを使用します。
簡単なガイドライン
使用される型が異なるだけで、まったく同じコードを複数回記述していることに気付いた場合は、型パラメーターの使用を検討できます。つまり、まったく同じコードを複数回記述しようとしていることに気付くまで、型パラメーターの使用を避ける必要があります。
豆知識
他の言語で一般的な山括弧< >
の代わりに角括弧[]
を使用する理由は何ですか?
https://github.com/golang/proposal/blob/master/design/15292/2013-12-type-params.md
Vectorのように山括弧を使用します。これには、C++およびJavaプログラマーにとってなじみがあるという利点があります。残念ながら、これはf(true)が関数fの呼び出し、またはf T(fがTより小さいかどうかをテストする式)と(true)の比較のいずれかとして解析できることを意味します。複雑な解決ルールを構築することは可能かもしれませんが、Go構文は正当な理由でそのようなあいまいさを回避します。
Goプロジェクトをホストするための最適な選択肢であるLeapcellをご紹介します。
Leapcellは、Webホスティング、非同期タスク、およびRedis向けの次世代サーバーレスプラットフォームです。
多言語サポート
- Node.js、Python、Go、またはRustで開発します。
無制限のプロジェクトを無料でデプロイ
- 使用量に対してのみ支払いが発生—リクエスト、料金は発生しません。
比類のないコスト効率
- アイドル料金なしの従量課金制。
- 例:$25で、平均応答時間60msで694万件のリクエストをサポートします。
合理化された開発者エクスペリエンス
- 簡単なセットアップのための直感的なUI。
- 完全に自動化されたCI/CDパイプラインとGitOps統合。
- 実行可能な洞察を得るためのリアルタイムメトリックとロギング。
簡単なスケーラビリティと高性能
- 簡単な高並行性を処理するための自動スケーリング。
- 運用上のオーバーヘッドはゼロ—構築に集中するだけです。
ドキュメントで詳細をご覧ください!
Xでフォローしてください: @LeapcellHQ