The Go gopher was designed by Renee French.
Go 1.27 ブログ連載 の 3 本目です。
はじめに
こんにちは。 CSIG (Cyber Security Innovation Group) の市川です。普段は、 FutureVuls の開発・運用に従事しており、最近はパフォーマンスの課題に主に取り組んでいます。
本記事では、 Go 1.27 より導入される「ジェネリックメソッド (generic methods)」について紹介します。
なお、本記事は go1.27rc1 時点の動作に基づいています。
ジェネリックメソッドの概要
以下の issue に、ジェネリックメソッドの Proposal がまとまっています。
できるようになったこと
Go 1.26 までは、型パラメータを宣言できるのは、トップレベルの関数と型のみでした。
メソッドはレシーバの型パラメータ (下の例では Stack[T] の T) を利用できるだけで、メソッド自身が新しい型パラメータを宣言することはできず、次のように書くと文法エラーになる仕様でした。
// Go 1.26 以前は syntax error: method must have no type parameters |
Go 1.27 ではこの制限が外れ、メソッドに対しても、型パラメータを宣言することができるようになりました。
type Stack[T any] struct{ items []T } |
引き続きできないこと
型パラメータを持つメソッドを、インターフェースのメソッドとして宣言することはできません。
例えば、以下のような書き方は NG です。
type Mapper[T any] interface { |
また、ジェネリックメソッドはインターフェースのメソッドの実装としては扱われません。
例えば、以下の例では、 S が Map メソッドを持っていても、I を満たしません。
package main |
コンパイルしようとすると、以下のエラーが出ます。
main.go:9:11: cannot use S{} (value of struct type S) as I value in variable declaration: S does not implement I (wrong type for method Map) |
実装までの経緯
いつから提案されていたか
ジェネリックメソッドについては、2021 年の時点で強い需要がありました。
以下は、中心となった提案です (執筆時点で 900 を超える 👍 が付いています)。
以下のように、「呼び出しごとに変換後の型を変えたい」という要望でした。
構造体側に不要な型パラメータを生やすことなくこの要望を満たすには、ジェネリックメソッドが必要でした。
// stream[IN] を stream[OUT] へ変換したい。OUT は呼び出しごとに変わる |
“This limitation prevents to define functional-like stream processing primitives … Allowing type parameters in methods would allow constructing DSLs that would greatly simplify some existing use cases.”
— mariomac, #49085 本文
また、Go 1.23 で iter.Seq[T] が導入されてからは、「seq.Map().Filter().Reduce() のようにイテレータ操作をメソッドチェーンで書きたい」という需要も生まれました。この需要は、Proposal (#77273) の議論の中で、次のように言語化されています。
“What people do want is to have methods like
Map/Filter/Reduceoniter.Seq[T], to allow chaining those calls and reading them left-to-right.”
— Merovius, #77273 (comment) (2026-02-15)
なぜジェネリクス導入時には見送られたのか
インターフェースにジェネリクスメソッドを定義することには、設計当初から根本的な実装上の壁があることが知られていました。
ジェネリックメソッドを提案するにあたり、この壁を乗り越える必要があり、次の 3 つの実装案が検討されましたが、いずれも却下されました。
| 実装案 | 内容 | 却下理由 |
|---|---|---|
| ① インターフェース内のジェネリックメソッドを許可し、メソッドをリンク時にインスタンス化する | 呼び出しグラフ全体を辿り、起こりうる全ての型引数を事前生成する | リンク時に呼び出しグラフ全体を見ても、reflection 経由の動的なメソッド呼び出しがある限り、必要な型引数の集合を確定できず、実現不可能。 |
| ② インターフェース内のジェネリックメソッドを許可し、メソッドを実行時にインスタンス化する | 呼び出し時に必要な実体を生成する | JIT 相当が必要。実装が非常に複雑で、実行時に驚くほど遅くなってしまう。Go の「ソースを事前にすべて機械語に変換する」という方式 (AOT) に反する。 |
| ③ インターフェースにジェネリックメソッドを生やすのを諦める | ジェネリックメソッドはインターフェースを実装しない、という制約を加える | それなら、メソッドである意味が薄い |
① は、実現不可能でした。(詳細は、以下の補足を参照してください)
また、 ② も実装が非常に困難である上に、Go の「ソースを事前にすべて機械語に変換する」という方式にも反しており、非現実的でした。
残る案 ③ ですが、当初、Go では、「メソッド = インターフェースを実装するもの」という考えが主流であり、「メソッドの存在意義の大部分はインターフェースの実装にあるので、インターフェースを満たさないならメソッドにする意味が不明確(トップレベル関数で十分)」と考えられていました。
そのため、③ 案も非現実的でした。
補足: なぜ Go がインターフェースのメソッドに型パラメータを許していないのか
Go ではコンパイル時にすべての型を決定する必要があります。
そのため、ジェネリックメソッドをインターフェースに定義できるようにすると、その呼び出しで必要になりうる全ての組み合わせ (= 無限通りの組み合わせ) のコードをコンパイル時に用意する必要があり、これは実質実装不可能です。
そのため、Go ではインターフェースのメソッドに型パラメータを許していません。 (こちらは現在も当てはまります)
実例を用いて、この困難さを説明します。
こちらで紹介する思考実験については、Type Parameters Proposal#No parameterized method を参照してください。
また、https://github.com/golang/go/issues/49085#issuecomment-2316352221 でも詳しく説明されているため、合わせて参照してください。
以下の HasIdentity のように、インターフェースにジェネリックメソッドを定義できると仮定します。
package p2
// HasIdentity is an interface that matches any type with a
// parameterized Identity method.
type HasIdentity interface {
IdentityT any T
}
このインターフェースのジェネリックメソッドがどのような問題を引き起こすかを理解するため、さらに以下のようなパッケージ群を考えます。
package p1
type S struct{}
func (S) Identity[T any](v T) T { return v }
package p3
func CheckIdentity(v interface{}) {
if vi, ok := v.(p2.HasIdentity); ok {
if got := vi.Identity[int](0); got != 0 {
panic(got)
}
}
}
package p4
func CheckSIdentity() {
p3.CheckIdentity(p1.S{})
}
Go では、実際に使われる型引数の組み合わせごとに、インスタンス化されたコード (この例では S.Identity[int] の実体) がコンパイル時に生成される必要があります。
しかし、上記のようなパッケージ群をコンパイルする際、 S.Identity[int] はどのパッケージも生成できません。
- p1 をコンパイルするとき: 「誰が
intで呼ぶか」が分からないため、Identity[int]を作れない - p3 をコンパイルするとき: p3 は
p1.Sの存在を知らないため、S.Identity[int]を作れない - p4 をコンパイルするとき: p4 からは p3.CheckIdentity の関数本体が見えないため、p4 自身は Identity の型パラメータに int が渡されることを知らず、
S.Identity[int]を作れない
このため、「コンパイル時に、HasIdentity が実際に取り得る型だけ作る」方法を取ることができません。
この状況下で、コンパイル時にすべての型を決定するためには、 S.Identity の実体を全部生成しておく必要があります。(S.Identity[int], S.Identity[string], S.Identity[Hoge], etc…)
これは無限個の実体を事前に生成しなければいけないことを意味しており、実質不可能です。
このように、「ソースを事前にすべて機械語に変換する」という方式のもとでコンパイル不可能になるのを避けるため、インターフェースにジェネリックメソッドを定義できないようになっているのです。
方針転換
2026 年 1 月に、Go の設計者の一人である Robert Griesemer が、③ の「インターフェースを諦める」案を再度提案しました。
これは、以下の結論に至ったことに起因します。
具象メソッドはインターフェースとは関係なく、それ自体で有用な言語機能である
この提案は無事受け入れられ、「インターフェースのジェネリックメソッド」は実装せず、「ジェネリックな具象メソッド」のみを実装するという方針で Go 1.27 に導入されることとなりました。
何が嬉しいか
個人的に一番嬉しいと感じるのは、メソッドチェーンで複数の処理を左から右へ素直に書けることです。
Stack[T] に Map と Filter を持たせる場面で考えます。
Filter のように型パラメータを増やさない操作は、Go 1.26 までもメソッドとして書けました。一方 Map には「変換後の型」を表す新しい型パラメータ U が必要です。メソッドは新しい型パラメータを宣言できなかったため、Map は Stack[T] を第一引数に取るパッケージ関数にするしかありませんでした。
// Go 1.26: Filter はメソッドにできるが、 |
// Go 1.27: どちらもメソッドとして書ける |
単発で呼ぶだけであれば、Map(stack, f) が stack.Map(f) になる程度の違いのため、後者の方が自然な書き方とはいえ、そこまで大きな差ではありません。
効いてくるのは、処理を続けて適用したい場合です。
Map ⇒ Filter ⇒ Map と処理を続けたい場合、Go 1.26 までは、Map が現れるたびにメソッドチェーンが関数呼び出しで分断され、以下のように入れ子になります。実行される順番 (内側の Map → Filter → 外側の Map) と読む順番が一致せず、続けて適用する処理が増えるほど、可読性が下がってしまいます。
words := &Stack[string]{items: []string{"go", "generics", "methods"}} |
Go 1.27 以降は、以下のように処理の順番どおりに、左から右へ素直に読めるメソッドチェーンの書き方が可能になります。
Java の Stream API と同じような書き心地が得られます。
words := &Stack[string]{items: []string{"go", "generics", "methods"}} |
まとめ
本記事では、ジェネリックメソッドを紹介しました。
ジェネリックメソッドの導入によってメソッドチェーンが書きやすくなり、今後、様々なライブラリでより便利なメソッドが提供されていくのではないでしょうか。
今後の Go の発展が非常に楽しみになる機能だと感じました。
明日は、辻さんの「json v2」の紹介です。