ジェネリックな関数
型パラメータは、通常の引数の前の角かっこに書きます。それぞれが、許される型を示す制約を持ちます。
T と U は引数から推論されるので、Map[int, string](...) と書くことはめったにありません。推論は関数の引数から行われ、結果にしか現れない型は推論できません。func Zero[T any]() T なら Zero[int]() と書く必要があります。
Go 1.18より前は、この関数を型ごとに書くか、[]interface{} を受け取って型安全性を失うしかありませんでした。
制約
制約はインターフェースです。使える型を制限し、その代わりに、その型の値に対してどの操作が許されるかをコンパイラに伝えます。
| 制約 | 許す型 | 使えるもの |
|---|---|---|
any | すべての型 | 代入、受け渡し、保存 |
comparable | == をサポートする型 | ==、!=、マップのキー |
cmp.Ordered(Go 1.21) | 整数、浮動小数点数、文字列 | <、<=、>、>=、加えて == |
| メソッドを持つインターフェース | そのメソッドを持つ型 | メソッドの呼び出し |
~int | ~float64 のような型の和集合 | ちょうどその型 | すべての型がサポートする演算子 |
comparable の代わりに any を使うと、v == target は invalid operation: v == target (incomparable types in type set) でコンパイルに失敗します。演算子を使えるようにしているのは制約です。
この2つのヘルパーはすでに存在します:slices.Index と slices.Max です。標準の slices、maps、cmp パッケージが日常的なジェネリクスの用途の大半をまかなうので、自分で書く前に確認しましょう。
型の和集合と~による独自の制約
制約は | で型を並べられます。使える演算子は、並べたすべての型がサポートするものです。
チルダが重要です。~int64 は「基底型が int64 である任意の型」という意味なので、名前付きの型 Cents が受け付けられます。単に int64 と書くと、Sum([]Cents{...}) は Cents does not satisfy Number (possibly missing ~ for int64 in Number) で失敗します。
型の和集合を含むインターフェースは制約としてしか使えず、普通の変数の型には使えません。var n Number はコンパイルできません。
golang.org/x/exp/constraints パッケージには、既製の Integer、Float、Signed の制約があります。標準ライブラリの外にあるので、ここの実行環境ではインポートできません。標準の制約は cmp.Ordered です。
ジェネリックな型
構造体、スライス、マップなどの型も型パラメータを持てます。ジェネリックな型のメソッドは、レシーバーの型パラメータを使います。
この例には、いつも出てくる細かな点が2つあります。
- Tのゼロ値。
var zero Tは、ジェネリックなコードから「何もない」を返す方法です。すべての型に使えるT{}やnilはありません。 - インスタンス化。 ジェネリックな型は使う前にインスタンス化する必要があります。
StackではなくStack[string]です。自身のメソッドの中では、レシーバーをStack[T]と書きます。
Go 1.24でジェネリックな型エイリアスが追加され、type Set[T comparable] = map[T]struct{} がコンパイルできるようになりました。
出会うことになる制限
- メソッドに型パラメータは付けられない。
func (s *Stack[T]) Map[U any](...)は許されません。トップレベルの関数を使います。 - 特殊化はできない。
T = stringのための別の実装は書けません。回避策は関数内でのany(v)に対する型スイッチですが、たいていはジェネリクスが適切な道具ではないというサインです。 - 名前で演算子の制約は書けない。 「
+メソッドを持つ任意の型」とは言えず、和集合で型を並べます。 - 制約を通したフィールドアクセスは使えない。 和集合のすべての型が
IDフィールドを持っていても、v.IDは許されません。代わりに制約にメソッドを入れます。
ジェネリクスを使うべきでないとき
ジェネリクスが適しているのは、コンテナ型(スタック、集合、キャッシュ、木)と、要素の型によらず同じ処理をするスライスやマップのアルゴリズムです。どこにでも合うわけではありません。
- 具体的な型が1つだけ。 関数が
[]Userでしか呼ばれないなら、ジェネリックな版は読む手間を増やすだけです。 - 型によって振る舞いが違う。 それはインターフェースの役目です。
io.Writerを受け取る関数は[T io.Writer]を受け取る関数より単純で、同じように動きます。 - インターフェースの引数を置き換える。
func Print[T fmt.Stringer](v T)はfunc Print(v fmt.Stringer)より良いところがありません。
Goチーム自身の指針は、まず特定の型のためのコードを書き、型だけを変えた同じコードを2回目に書いていると気づいたら型パラメータを使う、というものです。
よくある間違い
comparableやcmp.Orderedが必要な場所でanyを使う。 コンパイラはanyの型パラメータに対する==や<を拒否します。~を忘れる。type ID intのようなユーザー定義の型は、単なるintの制約を満たしません。- 型パラメータに対して
nilを返す。 制約がTをポインタのような型に限定していない限り許されません。ゼロ値を返します。 - すでに存在するジェネリックなヘルパーを書く。 まず
slices、maps、cmpを確認します。
よくある質問
Goにジェネリクスはありますか?
Go 1.18(2022年3月)からあります。関数と型は角かっこの中で型パラメータを宣言できます:func Map[T, U any](s []T, f func(T) U) []U。各型パラメータは制約を持ち、制約はその型がサポートすべきことを記述するインターフェースです。
Goのジェネリクスのanyとcomparableの違いは何ですか?
any はすべての型を許しますが、値に対しては代入、受け渡し、保存以外ほとんど何もできません。comparable は == と != をサポートする型だけを許し、マップのキーや等価比較に必要なのはこちらです。< と > には cmp.Ordered を使います。
Goの制約のチルダ(~)は何を意味しますか?
~T は「基底型がTである任意の型」という意味です。~int は int にも type Celsius int にも一致します。チルダがないと、制約の中の int は int そのものにしか一致しないので、ユーザー定義の型は拒否されます。
Goのメソッドは型パラメータを持てますか?
持てません。メソッドはレシーバーの型の型パラメータを使えます(func (s *Stack[T]) Push(v T))が、新しい型パラメータは宣言できません。代わりにトップレベルのジェネリックな関数を書きます:func Map[T, U any](s *Stack[T], f func(T) U) *Stack[U]。