Menu

Go言語のジェネリクス:型パラメータ、制約、使用例

ジェネリクス(Go 1.18以降)を使うと、1つの関数や型が型安全なまま多くの型で動きます。型パラメータ、any・comparable・cmp.Orderedの制約、~を使った独自の制約、ジェネリックな型、そして使うべきでない場面を解説します。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

ジェネリックな関数

型パラメータは、通常の引数の前の角かっこに書きます。それぞれが、許される型を示す制約を持ちます。

TU は引数から推論されるので、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 == targetinvalid operation: v == target (incomparable types in type set) でコンパイルに失敗します。演算子を使えるようにしているのは制約です。

この2つのヘルパーはすでに存在します:slices.Indexslices.Max です。標準の slicesmapscmp パッケージが日常的なジェネリクスの用途の大半をまかなうので、自分で書く前に確認しましょう。

型の和集合と~による独自の制約

制約は | で型を並べられます。使える演算子は、並べたすべての型がサポートするものです。

チルダが重要です。~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 パッケージには、既製の IntegerFloatSigned の制約があります。標準ライブラリの外にあるので、ここの実行環境ではインポートできません。標準の制約は 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回目に書いていると気づいたら型パラメータを使う、というものです。

よくある間違い

  • comparablecmp.Ordered が必要な場所で any を使う。 コンパイラは any の型パラメータに対する ==< を拒否します。
  • ~ を忘れる。 type ID int のようなユーザー定義の型は、単なる int の制約を満たしません。
  • 型パラメータに対して nil を返す。 制約がTをポインタのような型に限定していない限り許されません。ゼロ値を返します。
  • すでに存在するジェネリックなヘルパーを書く。 まず slicesmapscmp を確認します。

よくある質問

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である任意の型」という意味です。~intint にも type Celsius int にも一致します。チルダがないと、制約の中の intint そのものにしか一致しないので、ユーザー定義の型は拒否されます。

Goのメソッドは型パラメータを持てますか?

持てません。メソッドはレシーバーの型の型パラメータを使えます(func (s *Stack[T]) Push(v T))が、新しい型パラメータは宣言できません。代わりにトップレベルのジェネリックな関数を書きます:func Map[T, U any](s *Stack[T], f func(T) U) *Stack[U]

Coddy programming languages illustration

Coddyでコードを学ぼう

始める