Go のポインタレシーバのメソッドは呼べるのにインターフェースを実装しない理由:メソッドセットとアドレス可能性

c.Inc() はコンパイルできるのに、c をインターフェースに代入するとエラーになります。Go 仕様のメソッドセットとアドレス可能性の規則で理由を説明し、マップ要素・埋め込み・メソッド値で起きるケースを整理します。

gomethod-setpointer-receiverinterface

Go では、ポインタレシーバのメソッドを値の変数から呼び出すコードは問題なくコンパイルできます。ところが同じ変数を、そのメソッドを要求するインターフェースに代入するとコンパイルエラーになります。メソッドは確かにあるのに、インターフェースは実装していないというわけです。

この二つの挙動は仕様の別々の規則から来ています。インターフェースを実装するかどうかはメソッドセットで決まり、値からポインタメソッドを呼べるのはアドレス可能な値にだけ適用される呼び出しの規則のおかげです。この記事では二つの規則を分けて確認し、その差がマップ要素、関数の戻り値、埋め込み、メソッド値でどう表れるかを整理します。

基準にした文書は、2026-10-27 に確認した Go 言語仕様(Language version go1.27, May 26, 2026)、Go FAQ、Effective Go です。以下の規則は最近のバージョンで加わったものではなく、メソッドセットの基本的な定義から導かれます。ジェネリック型のメソッドは扱いません。例に書いた「コンパイルエラー」や結果は仕様の規則から導いたもので、この記事のために実際に実行した結果ではありません。コンパイラが出すエラーメッセージの文言は仕様で定められていないため、引用していません。

例に使う型

記事全体でこの型を使います。

type Counter struct{ n int }

func (c *Counter) Inc()      { c.n++ }      // ポインタレシーバ
func (c Counter) Value() int { return c.n } // 値レシーバ

type Incrementer interface{ Inc() }
var c Counter
c.Inc()                // コンパイルできる
var _ Incrementer = c  // コンパイルエラー
var _ Incrementer = &c // コンパイルできる

メソッドセット:T と *T は違う

仕様の「Method sets」節は、すべての型にメソッドセットがあり、それがその型のオペランドで呼び出せるメソッドを決めると説明しています。定義型についての規則は二つです。

型 メソッドセット
T レシーバが T のメソッド
*T(T はポインタでもインターフェースでもない) レシーバが *T または T のメソッド

Counter のメソッドセットには Value しかなく、*Counter のメソッドセットには Inc と Value の両方があります。*T のセットは T のセットを含みますが、逆は成り立ちません。

インターフェースの実装はこのセットで判定されます。仕様の「Interface types」節によると、メソッドだけを並べたインターフェースの型セットはそれらのメソッドをすべて実装する型の集合で、一つのメソッド指定の型セットは、メソッドセットにそのメソッドを含むすべての非インターフェース型です。インターフェースでない型 T は、インターフェースの型セットに属するときにそのインターフェースを実装します。Inc は Counter のメソッドセットにないので Counter は Incrementer を実装せず、*Counter は実装します。

それでも c.Inc() が通るのはなぜか

呼び出しには別の規則があります。仕様の「Calls」節は、x のメソッドセットに m があれば x.m() は有効だと定めたうえで、例外を一つ置いています。x がアドレス可能で、&x のメソッドセットに m があれば、x.m() は (&x).m() の省略形です。c は変数なのでアドレス可能で、c.Inc() は (&c).Inc() として扱われます。

この省略形はメソッド呼び出し(と後で見るメソッド値)にだけあります。インターフェースへの代入では、代入する値の型がインターフェースを実装しているかだけが問われ、代わりにアドレスを取ってくれる規則はありません。

逆向き、つまりポインタから値メソッドを呼ぶ場合はメソッドセットだけで説明がつきます。*Counter のメソッドセットに Value が含まれているからです。「Selectors」節の規則では、型 T または *T の値 x について x.f は T の中の該当するフィールドやメソッドを表し、仕様の例は p.M1()(レシーバは T1)を ((*p).T1).M1() のように、デリファレンスした値に対する呼び出しとして書き下しています。

インターフェースに自動の & がない理由

FAQ の「Why do T and *T have different method sets?」が理由を説明しています。インターフェース値がポインタ *T を持っていれば、メソッド呼び出しはポインタをデリファレンスして値を得られます。しかしインターフェース値が T の値を持っている場合、メソッド呼び出しがポインタを得る安全な方法はありません。それを許すとメソッドがインターフェースの中の値を書き換えられることになり、これは仕様が認めていません。

FAQ はさらに、コンパイラがアドレスを取れる場合であっても、メソッドが値を変更すればその変更は呼び出し側では失われると付け加えています。例として挙げられているのが次のコードです。

var buf bytes.Buffer
io.Copy(buf, os.Stdin) // 許されない

これが有効だとすると、標準入力は buf 自体ではなく buf のコピーに書き込まれます。FAQ は、それはほぼ望まれない動作なので言語が禁止していると説明しています。bytes.Buffer の Write はポインタレシーバなので、io.Copy(&buf, os.Stdin) のようにアドレスを渡します。

Effective Go はこの規則を一文にまとめています。値メソッドはポインタからも値からも呼べるが、ポインタメソッドはポインタからしか呼べない。そのうえで、値がアドレス可能なときは言語がアドレス演算子を自動で補うという便宜的な例外を説明しています。同じ文書の ByteSlice の例が fmt.Fprintf(&b, ...) と &b を渡しているのも、io.Writer を満たすのが *ByteSlice だけだからです。

アドレス可能でない値では呼び出しもできない

x.m() の省略形は x がアドレス可能なときにしか適用されないので、どの値がアドレス可能かが問題になります。仕様の「Address operators」節は、アドレス可能なオペランドを次のものに限っています。

  • 変数
  • ポインタの間接参照(*p)
  • スライスのインデックス操作(s[i])
  • アドレス可能な構造体オペランドのフィールドセレクタ
  • アドレス可能な配列のインデックス操作

このリストに入っていない代表的な値が、マップの要素、関数呼び出しの結果、複合リテラルです。

func newCounter() Counter { return Counter{} } // パッケージレベルの関数
s := []Counter{{}}
s[0].Inc() // コンパイルできる:スライスの要素はアドレス可能

m := map[string]Counter{"a": {}}
m["a"].Inc()   // コンパイルエラー
m["a"].Value() // コンパイルできる:値レシーバなのでアドレスは不要

newCounter().Inc() // コンパイルエラー

Counter{}.Inc()    // コンパイルエラー
(&Counter{}).Inc() // コンパイルできる

マップの要素について、仕様は「アドレスを取れない」と一文で書いてはいません。ただ、アドレス可能なオペランドのリストにマップのインデックス操作はなく、代入文の節も左辺オペランドを「アドレス可能なもの、マップのインデックス式、またはブランク識別子」と分けて並べています。マップ要素は代入はできてもアドレス可能なオペランドではない、というのはこの二つの規則を合わせて読んだ結論です。

関数の戻り値は、仕様のメソッド値の例にそのまま出てきます。makeT().Mp は makeT() の結果がアドレス可能でないため無効だと書かれています。呼び出し newCounter().Inc() も同じ条件に依存するので、同じ理由で通りません。

複合リテラルは少し事情が違います。「Address operators」節はアドレス可能性の要件の例外として、& のオペランドに複合リテラルを認めています。「Composite literals」節は、そのアドレスを取るとリテラルの値で初期化された一意の変数へのポインタが作られると説明しています。この例外は自分で & を書く場合のもので、リテラル自体をアドレス可能にするわけではありません。そのため Counter{}.Inc() には省略形が適用されず、(&Counter{}).Inc() は通ります。この点は仕様の定義をこう読んだ推論で、仕様に直接書かれた文ではありません。

マップの中の値をポインタメソッドで変更したいときは、よく使われる方法が二つあります。

// 1. 取り出して変更し、書き戻す
c := m["a"]
c.Inc()
m["a"] = c

// 2. 最初からポインタを格納する
pm := map[string]*Counter{"a": {}}
pm["a"].Inc() // pm["a"] は *Counter の値なので普通に呼べる

二つ目の方法でも pm["a"] 自体はアドレス可能ではありません。ただ値が *Counter で、そのメソッドセットに Inc があるので、省略形は必要ありません。

埋め込むとメソッドセットはどうなるか

仕様の「Struct types」節には、埋め込みフィールドから昇格したメソッドが外側の構造体のメソッドセットに入る規則が別にあります。構造体型 S と型名 T について:

  • S が T を埋め込むと、S と *S のメソッドセットはどちらもレシーバが T の昇格メソッドを含みます。*S はレシーバが *T の昇格メソッドも含みます。
  • S が *T を埋め込むと、S と *S のメソッドセットはどちらもレシーバが T または *T の昇格メソッドを含みます。
type Named struct{ Counter }     // 値で埋め込む
type NamedPtr struct{ *Counter } // ポインタで埋め込む

var n Named
var _ Incrementer = n  // コンパイルエラー:Named のメソッドセットに Inc がない
var _ Incrementer = &n // コンパイルできる

np := NamedPtr{&Counter{}}
var _ Incrementer = np // コンパイルできる

値で埋め込むと、外側の型も T と *T の区別をそのまま引き継ぎます。ポインタで埋め込むと、外側の構造体の値だけでポインタメソッドがメソッドセットに入ります。その代わり NamedPtr{} のように埋め込んだポインタが nil の状態も作れるので、どちらを選ぶかは nil のフィールドを扱う準備があるかどうかとあわせて決めることになります。

メソッド値とメソッド式

メソッドを呼び出さずに関数値として取り出すときも、同じ規則が当てはまります。仕様の「Method values」節は、呼び出しと同様に、アドレス可能な値からポインタレシーバのメソッドを参照すると自動でアドレスを取る(t.Mp は (&t).Mp と同じ)、ポインタから値レシーバのメソッドを参照すると自動でデリファレンスする(pt.Mv は (*pt).Mv と同じ)と書いています。

同じ節は、メソッド値を作る時点でレシーバの式を評価して保存し、後で呼び出すときはその保存したコピーをレシーバに使うと定めています。この規則を Counter に当てはめると次のようになります。

var c Counter
inc := c.Inc     // (&c).Inc:ポインタ &c が保存される
val := c.Value   // この時点の c の値(n == 0)がコピーされて保存される
inc()
inc()
fmt.Println(c.Value()) // 2
fmt.Println(val())     // 0

inc は c のアドレスを持っているので c を変更し、val は作ったときにコピーした値を使い続けます。仕様の例も、メソッド値を作った後で *t = 42 と書き換えても保存されたレシーバには影響しないことを示しています。

メソッド式では、自動の変換は一方向にしか働きません。仕様の「Method expressions」節によれば、値レシーバのメソッドからポインタをレシーバとして受け取る関数 (*T).Mv は作れますが、ポインタレシーバのメソッドを値レシーバの関数にする T.Mp は、ポインタレシーバのメソッドが値型のメソッドセットにないため認められません。(*Counter).Value は使えて、Counter.Inc は使えません。

インターフェースへのポインタにはメソッドがない

メソッドセットの二つ目の規則は、T がポインタでもインターフェースでもない場合にだけ適用されます。仕様はそれ以外の型のメソッドセットは空だと定めているので、*io.Writer のようなインターフェースへのポインタのメソッドセットは空です。FAQ の「When should I use a pointer to an interface?」も同じ結論です。var w io.Writer を fmt.Fprintf(&w, ...) と渡すとコンパイルできません。具象型へのポインタはインターフェースを満たせても、インターフェースへのポインタは(空インターフェースへの代入を除いて)インターフェースを満たせません。「インターフェースがポインタを求めているから & を付ける」という直し方が正しいのは、代入する値が具象型のときだけです。

レシーバの選び方

FAQ の「Should I define methods on values or pointers?」は、レシーバはメソッドの引数とまったく同じように振る舞うので、値とポインタのどちらにするかは関数の引数を値で受けるかポインタで受けるかと同じ問題だと説明しています。挙げられている考慮点は次のとおりです。

  • メソッドがレシーバを変更する必要があるなら、ポインタでなければなりません。スライスとマップは参照のように振る舞うので少し微妙ですが、メソッドの中でスライスの長さを変えるにはやはりポインタレシーバが必要です。
  • 大きな構造体のようにレシーバが大きいなら、ポインタレシーバのほうが安く済む場合があります。
  • 一貫性:一部のメソッドがポインタレシーバでなければならないなら、残りもポインタにそろえ、型の使われ方にかかわらずメソッドセットが一貫するようにします。
  • 基本型、スライス、小さな構造体のようにコピーが安い値は、メソッドの意味上ポインタが必要でなければ、値レシーバが効率的で明快です。

三つ目の点がこの記事の問題に直接つながります。Counter のようにレシーバを混ぜると Counter と *Counter のメソッドセットが変わり、*Counter だけが実装するインターフェースが出てきます。

型が意図したインターフェースを実装しているかは、FAQ が紹介している方法でコンパイル時に確認できます。

var _ Incrementer = (*Counter)(nil) // *Counter が Incrementer を実装しているか確認

Counter の値で実装すべきなら var _ Incrementer = Counter{} と書きます。この行がコンパイルできなければ、実装が足りていません。

まとめ

  • T のメソッドセットはレシーバが T のメソッド、*T のメソッドセットはレシーバが T または *T のメソッドです。インターフェースの実装はこのセットで判定されます。
  • 値の変数からポインタメソッドを呼べるのは、アドレス可能な x に対する呼び出しの省略形 (&x).m() のおかげで、インターフェースへの代入にはこの省略形がありません。
  • マップの要素、関数の戻り値、複合リテラルそのものはアドレス可能でないので、それらの上ではポインタメソッドを呼べません。マップにはポインタを格納するか、取り出して変更してから書き戻します。
  • T を埋め込むと区別を引き継ぎ、*T を埋め込むと外側の値もポインタメソッドを持ちます。
  • インターフェースへのポインタのメソッドセットは空です。

インターフェースに nil ポインタを入れたときに起きる別の問題は、Go で nil ポインタを入れた error が nil にならない理由で扱っています。

出典

いずれも 2026-10-27 に確認しました。

  • The Go Programming Language Specification (Language version go1.27, May 26, 2026): https://go.dev/ref/spec(「Method sets」「Interface types」「Struct types」「Selectors」「Calls」「Address operators」「Composite literals」「Assignment statements」「Method values」「Method expressions」)
  • Go FAQ: https://go.dev/doc/faq(「Why do T and *T have different method sets?」「Should I define methods on values or pointers?」「When should I use a pointer to an interface?」「How can I guarantee my type satisfies an interface?」)
  • Effective Go, “Pointers vs. Values”: https://go.dev/doc/effective_go(2009 年のリリース時に書かれ、積極的には更新されていないと文書内に記載あり)

ブログへ