タラバガニー設計局stalins.clubNOTE/notes/method-with-receiver

メソッドはレシーバを持つ関数

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.Mfunc(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つある。

  1. インターフェース充足。同じ形の関数をいくら書いてもメソッドセットには入らない (ジェネリクスメソッドはインターフェースを満たさない)
  2. 名前空間が違う。メソッド名は T / *T のセレクタの中でしか見えないので、Scale という関数と (*Point).Scale というメソッドが同じパッケージに共存できる
  3. レシーバの型に制限がある。defined type かそのポインタに限られ、同じパッケージでの宣言も必要。普通の引数にこの制約はない
  4. 自動アドレス取得と埋め込みによる昇格。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

▸ ノート一覧に戻る