Go 1.27のジェネリックメソッド—何が書けて、interfaceはなぜ満たせないか

Go 1.27のジェネリックメソッド—何が書けて、interfaceはなぜ満たせないか | mohablog

func (s Stream[T]) Map[U any](f func(T) U) Stream[U] が、Go 1.27 でコンパイルを通ります。1.26 までは syntax error: method must have no type parameters で弾かれていた書き方。解禁で何が変わり、何が変わらないのかを go1.27rc2 で確かめました。

目次

メソッドが自前の型パラメータを持てる

type Stream[T any] struct{ items []T }

func (s Stream[T]) Map[U any](f func(T) U) Stream[U] {
	out := make([]U, 0, len(s.items))
	for _, v := range s.items {
		out = append(out, f(v))
	}
	return Stream[U]{items: out}
}

レシーバの [T] は 1.26 までと同じ書き方。増えたのはメソッド名の後ろの [U any] です。レシーバ型とは無関係な型パラメータを、メソッドが自分で宣言できるようになりました。

Go 1.27 リリースノートの “Changes to the language” セクションはこう書いています。

Go 1.27 now supports generic methods: a method declaration may declare its own type parameters. This widely anticipated change allows adding generic functions within the namespace of a particular data type where before one had to declare such functions with a scope of the entire package.

パッケージ全体をスコープに持つ関数として置くしかなかったものを、特定のデータ型の名前空間に収める。これが解禁の主旨です。

1.26 までは関数に逃がすしかなかった

同じ処理を 1.26 で書くと、パッケージレベルの関数になります。

func MapFunc[T, U any](s Stream[T], f func(T) U) Stream[U] {
	out := make([]U, 0, len(s.items))
	for _, v := range s.items {
		out = append(out, f(v))
	}
	return Stream[U]{items: out}
}

メソッドにできなかった理由は単純で、Map[U any][U any] が構文エラーだったから。レシーバ由来の T は使えても、メソッド側で U を足せませんでした。

読む順番が内側からではなくなる

呼び出し側を並べます。

// 1.26: 内側から外側へ読む
r := MapFunc(MapFunc(s, toName), toLen)

// 1.27: 上から下へ読む
r := s.Map(toName).Map(toLen)

実際に動かした例。Filter で絞ってから、ユーザーを名前に、名前を文字数に変換します。

users := New(
	User{"sato", 34},
	User{"suzuki", 27},
	User{"takahashi", 41},
)

lens := users.
	Filter(func(u User) bool { return u.Age >= 30 }).
	Map(func(u User) string { return u.Name }).
	Map(func(s string) int { return len(s) }).
	Slice()

fmt.Println(lens)
$ go1.27rc2 run .
[4 9]

型は Stream[User]Stream[string]Stream[int] と乗り換わります。27歳の suzuki が Filter で落ち、sato の4文字と takahashi の9文字が残る。1つのチェーンの中で型パラメータが3回変わる形は、1.26 では関数のネストでしか書けませんでした。

1.26 で書くと3種類のエラーが出る

手元で踏んだものを並べます。

状況エラーメッセージ
Go 1.26 のコンパイラでビルドsyntax error: method must have no type parameters
go1.27rc2 だが go.mod が go 1.26generic method requires go1.27 or later (-lang was set to go1.26; check go.mod)
interface のメソッドに型パラメータを書いたinterface method must have no type parameters

go.mod を上げ忘れると2行目になる

引っかかりやすいのが2行目。ツールチェーンを 1.27 にしても、go.modgo ディレクティブが 1.26 のままだと言語機能のゲートで止まります。

$ go1.27rc2 build .
# gm
./main.go:10:24: generic method requires go1.27 or later (-lang was set to go1.26; check go.mod)

rc2 を入れた直後、この行の後半を読み飛ばして rc の不具合を疑い、15分ほど溶かしました。check go.mod まで書いてあるので、直す先はコンパイラではなく go.mod です。

コンパイラ側で落ちる場合との違い

1.26 のコンパイラに同じコードを渡すと、構文解析の段階で止まります。

$ GOTOOLCHAIN=local go build .
# gm
./main.go:10:23: syntax error: method must have no type parameters

前者は「バージョン設定の問題」、後者は「そもそも読めない」。区別がつくと、調べる場所を間違えずに済みます。

interface からは切り離されている

リリースノートは制約を1文で切っています。

Note that methods of interfaces may not declare type parameters nor can interface methods be implemented by generic methods.

interface のメソッドに型パラメータを書くこと。ジェネリックメソッドを interface の実装として使うこと。どちらも通りません。

宣言そのものが通らない

type Mapper[T any] interface {
	Map[U any](f func(T) U) []U
}
$ go1.27rc2 build .
# iface
./main.go:4:5: interface method must have no type parameters
./main.go:4:23: undefined: U

実態を示しているのは2行目の undefined: U です。型パラメータの宣言として認識されていないので、U はただの未定義の識別子として扱われる。

実装側も数えてもらえない

では interface 側を型パラメータ無しの素の形にして、具体型のジェネリックメソッドで満たそうとしたら。

type Box[T any] struct{ v T }

func (b Box[T]) Map[U any](f func(T) U) Box[U] { return Box[U]{v: f(b.v)} }
func (b Box[T]) Get() T                        { return b.v }

type Getter interface{ Get() int }
type HasMap interface{ Map(f func(int) int) Box[int] }

func main() {
	b := Box[int]{v: 7}
	var g Getter = b // 通常メソッドなので満たす
	var h HasMap = b // ジェネリックメソッドなので満たさない
	_, _ = g, h
}
./main.go:19:17: cannot use b (variable of struct type Box[int]) as HasMap value in variable declaration: Box[int] does not implement HasMap (wrong type for method Map)
		have Map[U any](func(int) U) Box[U]
		want Map(func(int) int) Box[int]

havewant の差が全てです。Map[U any](func(int) U) Box[U]U が決まるまで関数としての形が定まらない。対して HasMap が要求しているのは U=int に固定された1つの形。同じ名前でも別物として扱われます。

動的ディスパッチが成立しないから

制約はコンパイル方式から来ています。interface 越しの呼び出しは実行時に実体が決まる。そこにジェネリックメソッドを載せると、実行時に「まだ具体化していないメソッド」を呼ぶことになります。Go は AOT コンパイルなので、実行時に新しい実体を生成する仕組みを持ちません。

採用までの過程では、リンク時に全ての組み合わせを生成する案と、実行時に JIT で生成する案が検討されて落ちています。前者は reflect 経由の動的呼び出しがあるため型の列挙が完結しない。後者は AOT の前提を壊す。残ったのが「interface を諦めて具象メソッドだけ通す」という現在の形です。

Stream のような変換チェーンを interface で抽象化して実装を差し替える構成は、この制約により取れません。ジェネリックメソッドは具体型に閉じた便利機能として置く。抽象化の層を挟みたいなら、これまで通りパッケージレベルの関数に置くことになります。GoのインターフェースとDI設計パターンで組んだような差し替え前提の設計とは、そもそも扱う層が違います。

reflect からジェネリックメソッドは見えない

DI コンテナ、ORM のフック、モック生成ツール。この辺りは reflect で型のメソッドを走査します。ジェネリックメソッドはそこに出てきません。

b := Box[int]{v: 7} // Get と Map の2つのメソッドを持つ
t := reflect.TypeOf(b)
fmt.Println("NumMethod:", t.NumMethod())
for i := range t.NumMethod() {
	fmt.Println("  method:", t.Method(i).Name)
}
_, ok := t.MethodByName("Map")
fmt.Println("MethodByName(Map):", ok)
NumMethod: 1
  method: Get
MethodByName(Map): false

実体が無いものは列挙できない

Box[int] が持つメソッドは GetMap の2つ。reflect が返すのは Get だけで、MethodByName("Map")false です。

理屈は interface の話と同じ。U が決まっていないメソッドは機械語のレベルで実体を持ちません。reflect.Type.Method が返せるのは実体のあるメソッドだけ。Box[int] と型引数を埋めた状態で reflect.TypeOf に渡しても、MapU は未定のままです。

影響が出るのはメソッド走査に頼るコード

タグとメソッドを見て自動配線するタイプのライブラリは、ジェネリックメソッドを認識しません。reflect がどこまで見えているかの境界は Go structタグを読み解くで扱った範囲と同じで、型引数が解決済みかどうかで切れます。

レシーバがジェネリックでなくてもいい

型パラメータを持たない既存の struct にも、ジェネリックメソッドは足せます。

type Repo struct{ name string }

func (r Repo) Decode[T any](raw string, into *T) error {
	fmt.Printf("[%s] %s を %T にデコード\n", r.name, raw, *into)
	return nil
}
[users] 42 を int にデコード

レシーバは Repo のまま。ジェネリクスを入れるために型定義から書き換える必要はありません。ポインタレシーバでも同じで、func (c *Cache[K]) GetOr[V any](k K, fallback V) V のようにレシーバ由来の K とメソッド固有の V を混ぜられます。

実行コストは変わらない

要素数1000のスライスに2段の変換をかけて、関数のネストとメソッドチェーンを比較しました。

書き方ns/op(5回)B/opallocs/op
パッケージ関数のネスト1871 / 1882 / 1889 / 1898 / 1969164083
ジェネリックメソッドのチェーン1841 / 1880 / 1887 / 1900 / 1913164083

go1.27rc2 / darwin/arm64 / Apple M5 Pro / -benchmem -count=5。アロケーション回数もバイト数も一致しています。変わるのは書き味だけで、速度を目当てに書き換える理由はありません。

型推論が届かない形がある

引数に現れない型パラメータは埋まらない

戻り値にしか型パラメータが出てこないメソッドは、呼び出し側で明示する必要があります。

func (s Seq[T]) Zero[U any]() U {
	var z U
	return z
}

s.Zero()          // NG
s.Zero[float64]() // OK
./main.go:50:37: in call to s.Zero, cannot infer U (declared at ./main.go:29:22)

引数に U が現れる形なら推論が効きます。Reduce[U any](init U, f func(U, T) U) U は第1引数から U が決まるので、s.Reduce("", join) と書ける。iter.Seq を包んだ型MapReduce を繋いだ結果がこれです。

Reduce: 12345
Zero: 0

関数のまま残す判断

分かれ目は、interface を挟むかどうかと、reflect に拾わせているかどうか。

  • interface で実装を差し替える予定がある → パッケージレベルの関数のまま
  • reflect でメソッドを拾わせている → 関数のまま
  • 具体型に閉じた変換をチェーンで繋ぎたい → メソッドに移す
  • 依存ライブラリが go 1.27 に追随していない → 移行を待つ

そもそもジェネリクスを使うかどうかの判断は Goジェネリクスはいつ使うべきかで書いた通りで、メソッドに置けるようになっても基準は変わりません。

2026年8月6日時点で Go 1.27 の正式版はまだ出ていません。配布されているのは go1.27rc2 までで、stable の最新は go1.26.5。本番のビルドを go 1.27 に切り替えられるのは、正式版が出てからです。

まとめ

  • Go 1.27 でメソッドが自前の型パラメータを宣言できるようになった。レシーバの [T] とは別に [U any] を書ける
  • interface のメソッドに型パラメータは書けない。ジェネリックメソッドは interface の実装としても数えられない
  • reflect.Type.NumMethod はジェネリックメソッドを返さない。メソッド走査に頼るライブラリからは見えない
  • レシーバがジェネリック型である必要はない。既存の struct にそのまま足せる
  • 関数のネストが 1871〜1969 ns/op、メソッドチェーンが 1841〜1913 ns/op でアロケーションも同一。変わるのは呼び出しの読み順だけ
  • ツールチェーンだけ上げても、go.modgo ディレクティブが 1.26 なら弾かれる
よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次