1. そもそも receiver を選ぶ理由
次のような宣言を初めて見たとき、 func (t Task) Done() bool と func (t *Task) MarkDone()、 つい「えっと……アスタリスクがあるかないかの違いでしょ」と思いがちです。残念ながら違いはあります。しかも実用的です。 receiver は 2 つの問いに答えます。メソッドがコピーを扱うのか、それとも元の値を扱うのか。そしてそのメソッドがオブジェクトの状態を変更できるのか、です。 さらに、呼び出しのしやすさや性能にも関わるので、「とりあえず全部に * を付ける」だけでは最適とは言えません。
実践から切り離して議論しないために、学習用の小さなドメインとして「タスクトラッカー」を続けます。ここではタイトル、完了フラグ、タグの一覧を持つタスク (Task) を扱います。
最も重要なルール: Go ではすべて値渡し
このフレーズは、身につくまでは禅問答のように聞こえます。Go では関数やメソッドを呼ぶとき、引数はコピーされます。 receiver が Task なら、Task 全体がコピーされます。 receiver が *Task なら、コピーされるのは……ポインタ、つまり小さなアドレス値です。 でもそのアドレスは同じオブジェクトを指すので、その経由で元のフィールドを変更できます。
簡単なモデルを作ってみましょう:
package main
import "fmt"
type Task struct {
Title string
Done bool
}
func (t Task) MarkDoneCopy() {
t.Done = true // change the copy
}
func (t *Task) MarkDone() {
t.Done = true // change the original via pointer
}
func main() {
task := Task{Title: "Read Go spec", Done: false}
task.MarkDoneCopy()
fmt.Println(task.Done) // false
task.MarkDone()
fmt.Println(task.Done) // true
}
MarkDoneCopy は「仕事をしているように見える」のですが、結果は消えてしまいます。 変更が receiver のコピーの中で起きているからです。
2. receiver 選択の基準
Value receiver (T): メソッドはコピーを受け取る
Value receiver は、メソッドがデータを「読むだけ」で何も変えないときに向いています。これは書類のコピーを渡された状況に似ています。 見たり、マーカーで線を引いたり、余白に猫を描いたりはできますが、上司のファイルにある原本は傷つきません (上司にとっては幸いで、あなたにとってはそうでないかもしれません)。
Task の「読むだけ」のメソッドの例です:
package main
import "fmt"
type Task struct {
Title string
Done bool
}
func (t Task) IsDone() bool {
return t.Done
}
func main() {
task := Task{Title: "Buy milk", Done: true}
fmt.Println(task.IsDone()) // true
}
ここでは value receiver が最適です。メソッドはシンプルで、状態を変更せず、小さな構造体のコピーなどまったく問題になりません。
Pointer receiver (*T): アドレスを受け取り、元の値を変更できる
Pointer receiver は、メソッドが 状態を変更しなければならない ときに必要です。これが選択基準の中心で、ほとんどの場合はこれだけで十分です。 意味として「オブジェクトを変更する」メソッドなのに receiver が value だと、「変えたふり」になってしまいます。
タスクに戻って、ちゃんとした完了メソッドを作りましょう:
package main
import "fmt"
type Task struct {
Title string
Done bool
}
func (t *Task) MarkDone() {
t.Done = true
}
func main() {
task := Task{Title: "Write tests", Done: false}
task.MarkDone()
fmt.Println(task.Done) // true
}
API 設計の観点では自然に読めます。task.MarkDone() は本当にタスクを完了させるのであって、「そう見えるだけ」ではありません。
基準 1: メソッドが状態を変える → ほぼ常に *T
ここで、初心者がよくやる便利な「実例」を挙げます。たとえばタスク名変更メソッドを追加したのに、うっかり value receiver で書いてしまった場合です。
package main
import "fmt"
type Task struct {
Title string
}
func (t Task) RenameBad(newTitle string) {
t.Title = newTitle
}
func main() {
task := Task{Title: "Old"}
task.RenameBad("New")
fmt.Println(task.Title) // Old
}
感覚としては「保存を押したのに、保存されていない」に近いです。正しい版はこちらです:
package main
import "fmt"
type Task struct {
Title string
}
func (t *Task) Rename(newTitle string) {
t.Title = newTitle
}
func main() {
task := Task{Title: "Old"}
task.Rename("New")
fmt.Println(task.Title) // New
}
要するに、すべての「setter」「mark」「add」「clear」「フラグ切り替え」は、ほぼ常に *T です。
基準 2: 「重い」構造体はコピーしないほうがよい → しばしば *T
メソッド自体は状態を変えなくても、構造体が大きい場合があります。そのとき value receiver は技術的には正しいものの、割に合わないことがあります。 メソッド呼び出しのたびにオブジェクト全体をコピーするからです。すぐに燃え上がるわけではありませんが、パンを買いにトラックで行くようなものです。 できなくはないですが、妙です。
「重いオブジェクト」を模擬してみましょう(説明用です):
package main
import "fmt"
type Big struct {
Data [1024]byte
Name string
}
func (b Big) NameLen() int {
return len(b.Name)
}
func main() {
x := Big{Name: "abc"}
fmt.Println(x.NameLen()) // 3
}
Big は NameLen() のたびに丸ごとコピーされます。 実際の「重さ」は [1024]byte のせいだけではなく、 多数のフィールド、入れ子構造などによって生じます。その場合、*T を 「読むだけ」のメソッドに対しても選ぶことがよくあります。余計なコピーを避けるためです。
ただし極端に走ってはいけません。「これからはすべてのメソッドを常に *T にする」 ということです。value receiver のほうが API をシンプルかつ安全にできることもあります。だから基準は併用し、宗教のように 1 つだけを選ばないでください。
基準 3: API の一貫性は局所的な小細工より重要
ある型で半分のメソッドが T、残りが *T だと、 初心者は「このメソッドは呼べたのに、あのメソッドは呼べない」「これは変わったのに、あれは変わっていない」といった驚きの世界に住むことになります。 そのため Go では、明確な理由がない限り、各型について receiver スタイルをそろえるのが実践的なルールです。
よくある折衷案はこうです。状態を変える メソッドはすべて *T、 読むだけ のメソッドは T、ただし構造体が小さくてコピーを避ける必要がない場合に限ります。 構造体が「本格的」なら、多くのチームは読むだけのメソッドも含め、ほぼすべてを *T にします。統一感と性能のためです。
別の観点もあります(後で丁寧に触れます): receiver の選択は interface との互換性に影響します。というのも、pointer receiver のメソッドは 型 T の値からは常に「見える」わけではないからです。これは実コードですぐに表面化します。たとえば「型を契約に差し込もう」として コンパイラエラーが出るような場面です。
基準 4: ときには *T がフィールドのスライスと append のために必要
ここは少し繊細なポイントです。スライスは「ヘッダ」 (len/cap/pointer to array) だと考えると分かりやすいです。 ヘッダ自体は簡単にコピーされますが、そのために value receiver ではスライス長の変更を「失う」ことがあります。
タスクにタグを追加してみましょう:
package main
import "fmt"
type Task struct {
Title string
Tags []string
}
func (t Task) AddTagBad(tag string) {
t.Tags = append(t.Tags, tag) // change the copy of the slice header
}
func main() {
task := Task{Title: "Learn Go", Tags: []string{}}
task.AddTagBad("study")
fmt.Println(task.Tags) // []
}
タグは追加されていません。t はコピーであり、t.Tags の新しい値はメソッドの中に残ったままだからです。 mutating メソッドは *Task で書くべきです:
package main
import "fmt"
type Task struct {
Title string
Tags []string
}
func (t *Task) AddTag(tag string) {
t.Tags = append(t.Tags, tag)
}
func main() {
task := Task{Title: "Learn Go", Tags: []string{}}
task.AddTag("study")
fmt.Println(task.Tags) // [study]
}
この例はよく「悟りの瞬間」になります。append が変えるのは要素そのものではなく、潜在的にはヘッダ全体 (場合によっては新しい配列へ移動もする) だからです。pointer receiver がないと、仕事を「コピーの中で」やってしまいがちです。
Addressability: なぜ NewTask().MarkDone() はときどきコンパイルされないのか
もう 1 つ、コンパイラの気まぐれに見えるものがありますが、実際には奇妙な状況からあなたを守っています。 Pointer receiver には receiver が addressable であること、つまり「実際のアドレスを持つ値」であることが必要です。
例を見てください:
package main
type Task struct {
Done bool
}
func (t *Task) MarkDone() {
t.Done = true
}
func NewTask() Task {
return Task{Done: false}
}
func main() {
// NewTask().MarkDone() // does not compile: function result is not addressable
task := NewTask()
task.MarkDone()
}
なぜでしょうか。NewTask() は一時的な値を返すからです。コンパイラは、 (そして通常はしたがりませんが) あなたのために「隠れ変数」を作ってそのアドレスを取り、変更してすぐ捨てる、という魔法をする義務はありません。 対処法は単純です。例のように値を変数に保存してください。
「constructor」を pointer メソッドと相性よくしたいなら、次のようにすることがよくあります。 NewTask が *Task を返すようにします。
package main
import "fmt"
type Task struct {
Done bool
}
func (t *Task) MarkDone() {
t.Done = true
}
func NewTask() *Task {
return &Task{Done: false}
}
func main() {
task := NewTask()
task.MarkDone()
fmt.Println(task.Done) // true
}
たしかに、型の使い方は少し変わります。これ以降「task」はポインタとして生きることが多くなります。 でも mutating メソッドが多い型なら、それは自然なことです。
4. チートシート: receiver 選択の表とブロック図
メソッドを書くときは、次の簡単な頭の中の表を通してみてください。Go の「法」ではありませんが、習慣としてはとても役立ちます。
| メソッドに関する質問 | 「はい」の場合 | 「いいえ」の場合 |
|---|---|---|
| メソッドはフィールドを変更する必要がある? | |
次へ進む |
| メソッドはフィールド内のスライスに append する / map やスライスのフィールド全体を変更する? | |
次へ進む |
| 型が大きくて、コピーがもったいない? | 多くの場合 |
でもよい |
| API のスタイルを統一して予測しやすくしたい? | 多くの場合、すべてのメソッドを にする |
混ぜてもよいが、慎重に |
さらに視覚的にしたいなら、小さなブロック図があります:
flowchart TD
A[メソッドを書く] --> B{状態を変える?}
B -- yes --> P[*T]
B -- no --> C{中で append / フィールドのスライス全体を変更?}
C -- yes --> P
C -- no --> D{型が大きくてコピーがもったいない?}
D -- yes --> P
D -- no --> V[T]
この図は常識の代わりにはなりませんが、「ネットでそう言っていたから * を付けた」という状況からは救ってくれます。
なぜ標準ライブラリでもよく *T が選ばれるのか
pointer receiver は初心者の「裏技」みたいに見えることがありますし、「本当の Go 使いはきれいに書く」と思いたくなるかもしれません。 でも実際には pointer receiver はごく普通のもので、標準ライブラリでもよく使われます。特にメソッドがオブジェクトに論理的に結びついていて、 コピーしたくない場合や状態を変えたい場合です。
たとえば、型付きエラーは Error() メソッドを エラー構造体へのポインタに実装することがとても多いです。つまり func (e *NotFoundError) Error() string { ... } のようになります。 エラー設計自体はここでは詳しく扱いませんが、この事実は役に立ちます。 *T は汚い手口ではなく、普通の道具です。
5. よくあるミス
ミス 1: mutating メソッドを value receiver で書いてしまい、変更が「保存されない」。
最もよくあるバグです。メソッドの意味としてはオブジェクトを変えるべきなのに (Rename, MarkDone, AddTag)、receiver が T になっています。 コードはコンパイルされ、メソッドは呼ばれますが、状態はそのままです。対策は簡単です。メソッドがオブジェクトを変更すると約束しているなら、 *T にして、理由もなく return で「保存」しようとしないことです。
ミス 2: 「安全だから全部 *T にする」。
一見もっともらしいです。「ポインタのほうが強いのだから」と。でも別の問題を生みます。あちこちでポインタを運ぶことになり、 nil に出会うことが増え、値として扱えたはずのシンプルな型まで複雑になります。 ルールとしては、*T は mutating が必要なとき、またはコピー削減が必要なときに使い、「デフォルトだから」ではありません。
ミス 3: value receiver でフィールドのスライスに append して予想外の挙動になる。
このバグは特に厄介です。「タグを追加したじゃないか!」に見えるからです。しかし実際にはスライスヘッダのコピーにタグを追加しただけです。 その結果、外からは変更が見えません。メソッドが構造体フィールド内のスライスの長さや容量を変えるなら、ほぼ常に *T が必要です。
ミス 4: 一時的な値(アドレスを取れない値)に pointer メソッドを呼ぼうとする。
定番です。NewTask().MarkDone() に対してコンパイラが文句を言う、というやつです。 これは Go の意地悪ではなく、暗黙の魔法や奇妙な副作用を防ぐための保護です。正しい習慣は、 まず結果を変数に保存すること、または型に mutating メソッドが多いなら「constructor」が *T を返すように設計することです。
ミス 5: 分かりやすいルールのない、無秩序な receiver の混在。
一部のメソッドが T、別の一部が *T だと、数週間後には自分でもどこがどうなっていたか忘れます。 他人がコードを読むならなおさらです。どのメソッドが状態を変え、どれが変えないのかを推測させることになります。 型ごとに単純なルールを選んで、それに従うほうがよいです。「例外」は本当に必要な場面だけにしましょう。
GO TO FULL VERSION