メソッドはレシーバを持つ関数
Go 1.27 の提案 #77273 が採った見方は、メソッドをインターフェースの実装手段としてではなく「レシーバを持つ関数」として捉え直すことにある。実際、go1.27rc3 の doc/go_spec.html の Method declarations はこう書いている。
A method is a function with a receiver. The receiver is specified via an extra parameter section preceding the method name.
ここで言われているのは「第一引数」ではない。レシーバは引数リストとは別枠の「メソッド名の前に置く追加の引数節」として仕様に定義されている。一方 Method expressions の項は明示的に第一引数だと言う。T.M は func(tv T, a int) int になる、という書き方で、こちらは本当にレシーバを引数列の先頭に押し込む変換を指す (詳細は メソッド式とメソッド値)。
この違いは実測でも確認できる。go1.27rc3 で func (p *Point) Scale(f float64) と、同じ形の引数を持つ func Scale(p *Point, f float64) を並べると、go build -gcflags=-S の出力はどちらも LEAF|NOFRAME|ABIInternal, $0-16 で同じ形になり、go/ssa でもレシーバは Params[0] として現れて両者は完全に一致する。ところが go/types だけは別枠を維持していて、Signature.Params().Len() はメソッドが 1・関数が 2、レシーバは Recv() という別のアクセサに入っている。
つまり結論は「コンパイラから下では正しい。型システムから上では正しくない」。この境界線がそのまま、後半に出てくる「できないこと」の理由になる。「レシーバは単なる第一引数」というモデルでは説明が付かない点は少なくとも4つある。
- インターフェース充足。同じ形の関数をいくら書いてもメソッドセットには入らない (ジェネリクスメソッドはインターフェースを満たさない)
- 名前空間が違う。メソッド名は
T/*Tのセレクタの中でしか見えないので、Scaleという関数と(*Point).Scaleというメソッドが同じパッケージに共存できる - レシーバの型に制限がある。defined type かそのポインタに限られ、同じパッケージでの宣言も必要。普通の引数にこの制約はない
- 自動アドレス取得と埋め込みによる昇格。
v.Scale(2)は(&v).Scale(2)になり、埋め込めばメソッドが持ち上がる。関数呼び出しにはこの仕組みがない
提案本文の "concrete methods are a language feature that is useful in itself, irrespective of interfaces" は 1 を意図的に切り離す宣言であり、軸としては正しい。ただし「関数と全く同じになった」わけではないことは、関数だと通りメソッドだと通らないシグネチャ の instantiation cycle (同じシグネチャで関数は通りメソッドは落ちる) にも表れている。
出典
- Go Release Party 1.27 の発表資料リポジトリ
memo.md§1 - #77273