【Go】Go 1.27 ジェネリックメソッド — できること・できないこと

Go 1.27のジェネリックメソッド(型引数を持つメソッドとメソッドチェーン)を抽象化したアイキャッチ画像

Go 1.27のジェネリックメソッドでできることとできないことを、Go 1.27.1で実際に出たコンパイルエラーとあわせて整理します。インターフェース実装の壁やgo.modのバージョン制限も分かります。

目次

この記事が解決する悩み

  • Go 1.27 のジェネリックメソッドを、どこまで実務で使ってよいか判断したい
  • インターフェースに型引数を持たせられない理由と、実際に出るエラーを先に知っておきたい
  • go.mod のバージョン指定や CI で止まる条件を把握しておきたい

対象読者: Go 1.18 以降のジェネリクスを業務で使っている方。

前提知識: 型引数・型制約・メソッド式の基本。Go 1.27 の変更点を知っている必要はありません。

結論

Go 1.27 では、レシーバとは別にメソッド自身の型引数を宣言できるようになりました。

type List[E any] struct{ items []E }

// レシーバの型引数 E と、メソッド自身の型引数 R を併用できる
func (l List[E]) Map[R any](f func(E) R) List[R] { /* ... */ }

対象は具象型のメソッドだけです。インターフェースのメソッドは型引数を宣言できず、ジェネリックメソッドでインターフェースを実装することもできません。

  • できる: メソッドチェーン、メソッド式への変換、非ジェネリックメソッドとの併存
  • できない: インターフェースの実装、インターフェース側の型引数宣言
  • つまずきやすい: 型推論の効かない呼び出し、go.mod の go ディレクティブ

何が変わったのか

変わったのは「メソッド宣言が自分の型引数を宣言できる」という1点だけです。

Go 1.18 のジェネリクス提案は、インターフェースメソッドのジェネリック化が効率的に実装できないことを理由に、具象メソッドの型引数も一緒に見送っていました。

Go 1.27 はこの2つを切り離しました。経緯は提案 #77273 にまとまっています。

構文の見分け方は次の通りです。

宣言 型引数の持ち主
func (l List[E]) Get() E レシーバ(型 List の宣言)
func (l List[E]) Map[R any](f func(E) R) List[R] メソッド自身(Go 1.27 で追加)

両方で同じ名前は使えません。実測では E redeclared in this block で止まりました。

できること(Go 1.27.1 で実測)

チェーンが左から右に読めるのが気持ちいいところですね。次のコードは実際に動かしたものです。

func (l List[E]) Map[R any](f func(E) R) List[R] {
	out := make([]R, 0, len(l.items))

	for _, v := range l.items {
		out = append(out, f(v))
	}

	return List[R]{items: out}
}

func main() {
	fmt.Println(NewList(0, 2, 4).Map(add(2)).Map(divideBy(2)))

	// メソッド式に変換すると、従来の「内側から外側」の呼び方も復元できる
	f := List[int].Map[int]
	fmt.Println(f(f(NewList(0, 2, 4), add(2)), divideBy(2)))
}
[1 2 3]
[1 2 3]

Go 1.18 では MapList(MapList(NewList(0, 2, 4), add(2)), divideBy(2)) と内側から書く必要がありました。

標準ライブラリにも実例があります。math/rand/v2 の (*Rand).N は 1.27 でジェネリックメソッドになり、r.N(10) のように型引数を省略して呼べます。

非ジェネリックのメソッドとは同居できます。Filter のような既存メソッドを残したまま Map だけに型引数を持たせても問題ありません。

実測メモ: インターフェースを満たさないだけで、ジェネリックメソッド自体は普通に使えます。Generic型を埋め込んだ構造体(type IntList struct { List[int] })に昇格した Map も、型引数は引数の関数から推論できました。ビルドは通っています。なお stdversion vet はテスト実行時だけのチェックなので、go build ./... は成功し、go test ./... だけが失敗します。CI の最初のステップがビルドだけのプロジェクトは、ここを見落としやすいですね。

できないこと:インターフェースの壁

メソッドの見た目が一致しても、インターフェースを実装したことにはなりません。

type I interface {
	M()
}

type T struct{}

// シグネチャは M() と同じ形になるが、インターフェースは満たさない
func (T) M[P any]() {}

func main() {
	var i I = T{}
	fmt.Println(i)
}
cannot use T{} (value of struct type T) as I value in variable declaration:
	T does not implement I (wrong type for method M)
	have M[P any]()
	want M()

型 T が宣言しているのは T.M[int] ではなく、まだインスタンス化されていない T.M だからです。インターフェースの実装は型の性質で、個々のメソッドの性質ではありません。

インターフェース側に型引数を持たせようとすると、パースの時点で拒否されます。

interface method must have no type parameters

理由はコンパイルの分割にあります。インターフェース値への呼び出しは別パッケージで解決されうるため、呼ばれうるすべての型引数でインスタンス化したコードをあらかじめ用意できません。

型引数をボクシングすれば回避できますが、直接呼び出しにも間接呼び出しのコストが乗ります。Go 1.27 はこのトレードオフを避けて、具象型のメソッドだけを対象にしました。

インターフェース越しにジェネリックな操作を渡したい場合は、これまでどおりジェネリック関数を使います。

言語バージョンのゲート

go.mod の go ディレクティブが 1.27 未満だと、コンパイラが止めます。

generic method requires go1.27 or later
	(-lang was set to go1.26; check go.mod)

ファイル単位なら回避できます。go 1.26 のモジュールでも、//go:build go1.27 を付けたファイルの中ではジェネリックメソッドを書けます。

もう1つ、CI で先に気づくのはテストです。go test は stdversion vet チェックを既定で実行するようになりました。

rand.(*Rand).N requires go1.27 or later (module is go1.26)
FAIL	probe/stdver [build failed]

go build は通るのに go test だけが落ちます。go ディレクティブを据え置いたまま新しい標準ライブラリを触ると、ここで差が出ます。

実践ユースケース

変換のチェーンを型ごとに閉じる

パッケージスコープの変換関数を量産せずに済みます。MapList のような関数が何種類も並ぶと、名前空間が埋まってしまいますからね。

func (l List[E]) Map[R any](f func(E) R) List[R] { /* ... */ }

func main() {
	fmt.Println(NewList(0, 2, 4).Map(add(2)).Map(divideBy(2)))
}

変換関数を引数で差し替える

同じデータに対して、変換だけを差し替えたい場面はよくあります。制約を ~string にしておくと、文字列に変換する関数をそのまま渡せます。

func (l List[E]) ToString[R ~string](f func(E) R) []string {
	out := make([]string, 0, len(l.items))

	for _, v := range l.items {
		out = append(out, string(f(v)))
	}

	return out
}

func main() {
	words := NewList([]byte("Hallo Welt"), []byte("Helló világ"))
	fmt.Println(words.ToString(hex.EncodeToString))
	fmt.Println(words.ToString(base64.StdEncoding.EncodeToString))
}
[48616c6c6f2057656c74 48656c6cc3b32076696cc3a167]
[SGFsbG8gV2VsdA== SGVsbMOzIHZpbMOhZw==]

nil レシーバでも使える

ポインタレシーバのジェネリックメソッドは nil でも呼べます。ゼロ値の扱いをメソッド側に寄せられますよ。

var nilList *collection.List[int]
fmt.Println(nilList.Map(strconv.Itoa), nilList.Len())
// 出力: (nil) 0

つまずきポイント

  • cannot use generic function b.Cast without instantiation — メソッド値を取るときもインスタンス化が必要です。Box[int].Cast[string] のように型引数まで書いてください。
  • in call to Box[int]{...}.Cast, cannot infer R — 戻り値にしか現れない型引数は推論できません。Box[int]{}.Cast[string]() と明示します。
  • E redeclared in this block — レシーバの型引数とメソッドの型引数で同じ名前は使えません。
  • interface method must have no type parameters と does not implement I (wrong type for method M) — インターフェース側の型引数は書けず、ジェネリックメソッドはインターフェースを満たしません。
  • requires go1.27 or later (module is go1.26) — go build は通っても go test が vet で落ちます。

【広告】お名前.comならドメイン取得が格安。

検証環境

OS Windows 11 (build 10.0.26200)
CPU AMD Ryzen 9 PRO 8945HS w/ Radeon 780M Graphics
メモリ 28GB
言語/ツール Go 1.27.1(go.dev の公式ポータブル配布 go1.27.1.windows-amd64.zip を一時ディレクトリに展開)
使用コマンド go run(basic / usecase / xpkg / taggate) / go build(ifaceimpl / ifacedecl / versiongate / inferfail / shadow / methodval / stdver) / go test ./…(stdversion vet)
測定日 2026-09-18

上記の環境で実際に実行して確認した結果です。環境が異なる場合は挙動が変わることがあります。

Go 1.27.1 にジェネリックメソッドの各パターンをコンパイル・実行させた実測ログ。チェーンとメソッド式、標準ライブラリの (*Rand).N、埋め込み型の昇格、//go:build go1.27 での言語バージョン回避、インターフェース非対応の4種のエラー(wrong type for method M / interface method must have no type parameters / requires go1.27 or later / cannot infer R)までを1枚にまとめたもの
Go 1.27.1 にジェネリックメソッドの各パターンをコンパイル・実行させた実測ログ。チェーンとメソッド式、標準ライブラリの (*Rand).N、埋め込み型の昇格、//go:build go1.27 での言語バージョン回避、インターフェース非対応の4種のエラー(wrong type for method M / interface method must have no type parameters / requires go1.27 or later / cannot infer R)までを1枚にまとめたもの

まとめ

やりたいこと 可否 代替
具象型のメソッドに型引数を持たせる できる –
インターフェースのメソッドに型引数を持たせる できない ジェネリック関数を使う
ジェネリックメソッドでインターフェースを実装する できない 型引数の無いメソッドを併設する
メソッドを関数値に変換する できる List[int].Map[int] と明示する

「インターフェースは諦めて、メソッドの整理に使う」というのが Go 1.27 の割り切りです。そのぶんメソッドチェーンが読みやすくなりました。

参考

あわせて読みたい

【広告】このサイトはConoHa WINGで運営しています。安定した高速サーバーで快適にブログを書けています。いつもありがとう!!

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次