- Go言語
Go構造体とメソッドの書き方|値レシーバとポインタレシーバの違い
Goの構造体(struct)とは、複数の値をひとまとめにした型で、メソッドとは、その型に結び付けた関数です。 メソッドの「誰に対して呼ぶか」を表す部分をレシーバと呼び、func (c Counter) ... と書く値レシーバと、func (c *Counter) ... と書くポインタレシーバの2種類があります。値レシーバはコピーに対して動き、ポインタレシーバは呼び出し元そのものに対して動く、というのが一番大きな違いです。
この記事では、構造体の定義・初期化・メソッド・埋め込みまでを順番に通し、すべてのコードに go1.27.1 で動かした実行結果を付けています。値レシーバで変更が反映されない様子をアドレスつきで確かめ、ポインタレシーバのメソッドを持つ型を値のまま interface に入れたときのコンパイルエラーを、実際のメッセージのまま載せました。最後に、どちらのレシーバを選ぶかの判断表を置いています。
構造体とメソッドの早見表
書き方の早見表
| やりたいこと | コード | ポイント |
|---|---|---|
| 構造体を定義する | type Member struct { ID int; Name string } |
大文字で始めたフィールドはパッケージの外から見える |
| ゼロ値で作る | var m Member |
全フィールドがゼロ値(0、空文字、nil) |
| フィールド名つきで作る | m := Member{ID: 1, Name: "佐藤"} |
書かなかったフィールドはゼロ値。まずこの形を使う |
| 順番で作る | m := Member{2, "鈴木", "s@example.com", nil} |
全フィールドを定義順に書く必要がある |
| ポインタで作る | m := &Member{ID: 3} |
型は *Member |
| new で作る | m := new(Member) |
ゼロ値のポインタ。&Member{} と同じ結果 |
| コンストラクタ関数で作る | m, err := NewMember(5, "伊藤") |
入力の確認や初期値の設定をまとめられる |
| フィールドを読む・書く | m.Name、m.Name = "田中" |
ポインタでも m.Name と書ける |
| 値レシーバのメソッド | func (c Counter) Label() string |
c はコピー。読むだけの処理に向く |
| ポインタレシーバのメソッド | func (c *Counter) Add() |
c は呼び出し元を指す。変更が反映される |
| 埋め込み | type Article struct { Timestamps; Title string } |
埋め込んだ型のフィールドとメソッドを直接呼べる |
| 中身つきで表示 | fmt.Printf("%+v\n", m) |
フィールド名つきで表示 |
値レシーバとポインタレシーバの違い
| 比べる点 | 値レシーバ (c Counter) |
ポインタレシーバ (c *Counter) |
|---|---|---|
| メソッドの中の c | 呼び出し元のコピー | 呼び出し元を指すポインタ |
| フィールドの変更 | 呼び出し元に反映されない | 呼び出し元に反映される |
| 呼ぶたびのコピー | 構造体全体をコピーする | ポインタ1つぶんだけ |
変数 c から呼ぶ |
c.Label() |
c.Add()(自動で (&c).Add() になる) |
ポインタ p から呼ぶ |
p.Label()(自動で (*p).Label() になる) |
p.Add() |
| interface に入れられるもの | 値 Counter{} とポインタ &Counter{} の両方 |
ポインタ &Counter{} だけ |
| mapの値やリテラルから直接呼ぶ | 呼べる | コンパイルエラー |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Go構造体とメソッドの書き方|値レシーバとポインタレシーバの違い」
この記事の前提と動かし方
コードはどれも1ファイルで完結していて、main.go に貼って go run main.go で動きます。
| 項目 | 内容 |
|---|---|
| 実行したGo | go1.27.1 linux/amd64 |
| 実行コマンド | go run main.go |
| 使うパッケージ | 標準ライブラリの fmt だけ |
| 出力の扱い | 出力はそのまま掲載。panicのスタックトレースに出るファイルのパスだけ /home/taro/sample/ に置き換えています。アドレス(0x…)は実行のたびに変わります |
メソッドセットの定義と、レシーバの選び方の指針は、Go公式サイトのFAQ(go.dev/doc/faq)の「Why do T and *T have different method sets?」「Should I define methods on values or pointers?」を今回開いて確かめました。
構造体を定義して初期化する
構造体は type 名前 struct { ... } で定義し、初期化はフィールド名つきリテラルを基本にします。 6通りの作り方と、表示・コピー・比較を1つのコードにまとめました。
package main
import "fmt"
// 会員を表す構造体
type Member struct {
ID int
Name string
Email string
Tags []string
}
// コンストラクタ関数(Goの慣習で New + 型名)
func NewMember(id int, name string) (*Member, error) {
if name == "" {
return nil, fmt.Errorf("name is required")
}
return &Member{ID: id, Name: name, Tags: []string{}}, nil
}
func main() {
// 1. ゼロ値
var m0 Member
fmt.Printf("%+v\n", m0)
// 2. フィールド名つきリテラル(おすすめ)
m1 := Member{ID: 1, Name: "佐藤"}
fmt.Printf("%+v\n", m1)
// 3. 順番で指定するリテラル(全フィールド必須)
m2 := Member{2, "鈴木", "suzuki@example.com", nil}
fmt.Printf("%+v\n", m2)
// 4. ポインタで作る
m3 := &Member{ID: 3, Name: "高橋"}
m3.Email = "takahashi@example.com"
fmt.Printf("%+v\n", *m3)
// 5. new
m4 := new(Member)
m4.Name = "田中"
fmt.Printf("%+v\n", *m4)
// 6. コンストラクタ関数
m5, err := NewMember(5, "伊藤")
fmt.Printf("%+v %v\n", *m5, err)
_, err = NewMember(6, "")
fmt.Println(err)
// 表示の違い
fmt.Printf("%v\n", m1)
fmt.Printf("%+v\n", m1)
fmt.Printf("%#v\n", m1)
// コピーと比較
type Point struct{ X, Y int }
p1 := Point{1, 2}
p2 := p1
p2.X = 100
fmt.Println(p1, p2, p1 == p2, p1 == Point{1, 2})
// 無名構造体
conf := struct {
Host string
Port int
}{"localhost", 8080}
fmt.Println(conf.Host, conf.Port)
}
実行結果:
{ID:0 Name: Email: Tags:[]}
{ID:1 Name:佐藤 Email: Tags:[]}
{ID:2 Name:鈴木 Email:suzuki@example.com Tags:[]}
{ID:3 Name:高橋 Email:takahashi@example.com Tags:[]}
{ID:0 Name:田中 Email: Tags:[]}
{ID:5 Name:伊藤 Email: Tags:[]} <nil>
name is required
{1 佐藤 []}
{ID:1 Name:佐藤 Email: Tags:[]}
main.Member{ID:1, Name:"佐藤", Email:"", Tags:[]string(nil)}
{1 2} {100 2} false true
localhost 8080
初期化の方法を選ぶ
| 方法 | 書き方 | 得られる型 | 向いている場面 |
|---|---|---|---|
| ゼロ値 | var m Member |
Member |
あとから少しずつ値を入れるとき |
| フィールド名つきリテラル | Member{ID: 1, Name: "佐藤"} |
Member |
ふだん使う基本の形 |
| 順番で指定するリテラル | Member{2, "鈴木", "...", nil} |
Member |
Point{1, 2} のようにフィールドが少なく意味が明らかなとき |
| ポインタのリテラル | &Member{ID: 3} |
*Member |
ポインタレシーバのメソッドを使う型 |
| new | new(Member) |
*Member |
ゼロ値のポインタがほしいとき |
| コンストラクタ関数 | NewMember(5, "伊藤") |
関数が決める | 入力の確認や初期値の設定をまとめたいとき |
フィールド名つきリテラルをすすめる理由は、フィールドを足しても既存のコードがそのまま動くからです。順番で指定する形は、フィールドが1つ増えると全部の呼び出しを書き直すことになります。
newと&T{}の違い
new(Member) と &Member{} は、どちらも「ゼロ値の Member を指すポインタ」を返します。出力の5行目({ID:0 Name:田中 ...})のとおり、new で作ったあとにフィールドへ代入して使います。初期値を一緒に書けるのは &Member{...} の方なので、構造体ではこちらを見かけることが多くなります。
コンストラクタ関数を用意する
Goには、クラスのコンストラクタにあたる専用の構文がありません。代わりに、New + 型名という名前の普通の関数を用意するのが慣習です。コンストラクタ関数には次の役割を持たせられます。
| 役割 | 上のコードでの例 |
|---|---|
| 入力を確かめる | Name が空なら error を返す(出力7行目の name is required) |
| nil のままだと扱いにくいフィールドを初期化する | Tags: []string{}、map なら make(...) |
| ポインタで返すか値で返すかを1か所で決める | *Member を返す |
mapのフィールドは特に、ゼロ値(nil)のまま代入すると止まります。mapを持つ構造体では、コンストラクタ関数の中で make しておくと安心です。
表示の書式を使い分ける
出力の8〜10行目が、同じ m1 を3つの書式で表示した結果です。
| 書式 | 出力 | 使いどころ |
|---|---|---|
%v |
{1 佐藤 []} |
値だけを手短に見る |
%+v |
{ID:1 Name:佐藤 Email: Tags:[]} |
フィールド名つき。動作の確認に便利 |
%#v |
main.Member{ID:1, Name:"佐藤", Email:"", Tags:[]string(nil)} |
Goのコードに近い形。nil と空の違いまで見える |
代入はコピー、比較は==
p2 := p1 は構造体全体のコピーです。出力11行目のとおり、p2.X = 100 としても p1 は {1 2} のままです。また、すべてのフィールドが比較できる型なら、構造体同士を == で比べられます。 スライスやmapをフィールドに持つ構造体(上の Member は Tags []string を持つ)は == で比べられないので、フィールドごとに比べるか、slices.Equal などを組み合わせます。
メソッドを定義する:値レシーバとポインタレシーバ
メソッドは、func と関数名のあいだにレシーバを書いた関数です。 同じ構造体に、値レシーバとポインタレシーバの両方で「カウントを1増やす」メソッドを書いて、違いを確かめます。
package main
import "fmt"
type Counter struct {
Name string
Count int
}
// 値レシーバ:c は呼び出し元のコピー
func (c Counter) AddValue() {
c.Count++
fmt.Printf(" AddValue の中: Count=%d (アドレス %p)\n", c.Count, &c)
}
// ポインタレシーバ:c は呼び出し元を指す
func (c *Counter) AddPointer() {
c.Count++
fmt.Printf(" AddPointer の中: Count=%d (アドレス %p)\n", c.Count, c)
}
// 読むだけなら値レシーバでも結果は同じ
func (c Counter) Label() string {
return fmt.Sprintf("%s=%d", c.Name, c.Count)
}
func main() {
c := Counter{Name: "閲覧数"}
fmt.Printf("呼び出し元: Count=%d (アドレス %p)\n", c.Count, &c)
c.AddValue()
fmt.Println("AddValue のあと:", c.Label())
c.AddPointer() // (&c).AddPointer() と同じ意味に読み替えられる
fmt.Println("AddPointer のあと:", c.Label())
p := &c
p.AddPointer()
fmt.Println(p.Label()) // (*p).Label() と同じ意味
}
実行結果:
呼び出し元: Count=0 (アドレス 0x29e819b92030)
AddValue の中: Count=1 (アドレス 0x29e819b92048)
AddValue のあと: 閲覧数=0
AddPointer の中: Count=1 (アドレス 0x29e819b92030)
AddPointer のあと: 閲覧数=1
AddPointer の中: Count=2 (アドレス 0x29e819b92030)
閲覧数=2
アドレスに注目します。
| 行 | アドレス | わかること |
|---|---|---|
| 呼び出し元 | 末尾 …030 | 変数 c の場所 |
| AddValue の中 | 末尾 …048(別の場所) | c はコピー。中で Count を 1 にしても、呼び出し元は 0 のまま |
| AddPointer の中 | 末尾 …030(同じ場所) | c は呼び出し元を指している。Count が 1 になり、呼び出し元にも反映される |
flowchart LR
subgraph 値レシーバ
A["呼び出し元 c<br>Count=0"] -- コピー --> B["メソッド内の c<br>Count=1"]
end
subgraph ポインタレシーバ
C["呼び出し元 c<br>Count=0 → 1"]
D["メソッド内の c<br>(アドレス)"] -- 指す --> C
end
「値レシーバのメソッドで値を変えたのに、呼び出し元に反映されない」という動きは、コンパイルエラーにも実行時エラーにもなりません。プログラムは最後まで動き、結果だけが想定と違う形で現れます。フィールドを書き換えるメソッドは、ポインタレシーバで書きます。
c.AddPointer() と書けるのはコンパイラの読み替え
AddPointer のレシーバは *Counter ですが、変数 c(型は Counter)から c.AddPointer() と呼べています。これは、アドレスを取れる変数に対しては、コンパイラが (&c).AddPointer() と読み替えてくれるためです。逆に、ポインタ p から値レシーバのメソッドを呼ぶ p.Label() は、(*p).Label() と読み替えられます。
| 呼び出し方 | 値レシーバのメソッド | ポインタレシーバのメソッド |
|---|---|---|
変数 c(アドレスを取れる) |
そのまま呼べる | (&c). に読み替えて呼べる |
ポインタ p |
(*p). に読み替えて呼べる |
そのまま呼べる |
mapの値 m["a"]、リテラル Counter{} |
そのまま呼べる | コンパイルエラー(アドレスを取れない) |
3行目の「アドレスを取れない値」の場合を、次の節で実際に動かします。
コンパイルエラーになる書き方と直し方
構造体とメソッドまわりでよく出会うコンパイルエラーを、4つまとめて再現します。
package main
import "fmt"
type Counter struct{ Count int }
func (c *Counter) Add() { c.Count++ }
type Member struct {
ID int
Name string
}
func main() {
m := map[string]Counter{"a": {}}
m["a"].Add()
Counter{}.Add()
x := Member{1}
y := Member{Nmae: "佐藤"}
fmt.Println(m, x, y)
}
実行結果(コンパイルエラー):
# command-line-arguments
./main.go:16:9: cannot call pointer method Add on Counter
./main.go:17:12: cannot call pointer method Add on Counter
./main.go:18:15: too few values in struct literal of type Member
./main.go:19:14: unknown field Nmae in struct literal of type Member
| エラー文 | 原因 | 直し方 |
|---|---|---|
cannot call pointer method Add on Counter(16行目) |
mapの値はアドレスを取れないので、(&m["a"]).Add() に読み替えられない |
mapの値をポインタにする(map[string]*Counter) |
cannot call pointer method Add on Counter(17行目) |
リテラル Counter{} は変数ではないのでアドレスを取れない |
変数に入れてから呼ぶ。または (&Counter{}).Add() |
too few values in struct literal of type Member |
順番で指定するリテラルは全フィールドが必要 | Member{ID: 1} のようにフィールド名を付ける |
unknown field Nmae in struct literal of type Member |
フィールド名のつづりが違う | 定義と同じ名前にする |
直した形と、関連する動きをまとめて確かめます。
package main
import "fmt"
type Counter struct{ Count int }
func (c *Counter) Add() { c.Count++ }
type Node struct{ Name string }
func (n *Node) Label() string {
if n == nil {
return "(なし)"
}
return n.Name
}
type Box struct {
Label string
Items []string
}
func (b Box) Rename(s string) { b.Label = s }
func (b Box) SetFirst(s string) { b.Items[0] = s }
func main() {
// mapの値はポインタにすると更新できる
m := map[string]*Counter{"a": {}}
m["a"].Add()
fmt.Println(m["a"].Count)
// スライスの要素はアドレスが取れるのでそのまま呼べる
list := []Counter{{}, {}}
list[0].Add()
for _, c := range list {
c.Add() // c はコピー
}
fmt.Println(list)
for i := range list {
list[i].Add()
}
fmt.Println(list)
// nil ポインタでもメソッドは呼べる
var n *Node
fmt.Println(n.Label())
// 値レシーバでもスライスの中身は共有される
b := Box{Label: "A", Items: []string{"x", "y"}}
b.Rename("B")
b.SetFirst("変更")
fmt.Printf("%+v\n", b)
}
実行結果:
1
[{1} {0}]
[{2} {1}]
(なし)
{Label:A Items:[変更 y]}
出力から読み取れることを整理します。
| 出力 | わかること |
|---|---|
1 |
mapの値を *Counter にすると、m["a"].Add() が呼べて結果も残る |
[{1} {0}] |
スライスの要素はアドレスを取れるので list[0].Add() が呼べる。一方 for _, c := range list の c はコピーなので、c.Add() は元のスライスに反映されない |
[{2} {1}] |
添字で list[i].Add() と呼ぶと反映される |
(なし) |
nil ポインタに対してもメソッドは呼べる。メソッドの中で nil を確かめていれば止まらない |
{Label:A Items:[変更 y]} |
値レシーバでも、スライスの中身は共有されるので書き換わる。Label は変わらない |
最後の行は見落としやすい点です。値レシーバで構造体をコピーしても、スライス・map・ポインタのフィールドは「指している先」が同じです。b.Label = s は反映されず、b.Items[0] = s は反映されるという、半分だけ変わる状態になります。変更するメソッドをポインタレシーバにそろえておけば、こうした食い違いは起きません。
nilポインタでフィールドに触れると止まる
nil ポインタからメソッドを呼ぶこと自体はできますが、メソッドの中でフィールドに触れるとpanicになります。
package main
import "fmt"
type Counter struct{ Count int }
func (c *Counter) Add() { c.Count++ }
func main() {
var c *Counter
c.Add()
fmt.Println(c)
}
実行結果:
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x499df1]
goroutine 1 [running]:
main.(*Counter).Add(...)
/home/taro/sample/main.go:7
main.main()
/home/taro/sample/main.go:11 +0x11
exit status 2
invalid memory address or nil pointer dereference は、nil のポインタの先を読み書きしたときのメッセージです。スタックトレースの main.(*Counter).Add から、どのメソッドの中で起きたかを読み取れます。var c *Counter と宣言しただけの変数は nil なので、c := &Counter{} か NewCounter() のようなコンストラクタ関数で作ってから使います。
メソッドセットとinterface:値では満たせない場合
ポインタレシーバのメソッドを持つ型は、値のままでは interface に代入できません。 これがレシーバ選びで最もつまずきやすい点です。
package main
import "fmt"
type Resetter interface {
Reset()
}
type Counter struct{ Count int }
func (c *Counter) Reset() { c.Count = 0 }
func main() {
c := Counter{Count: 5}
var r Resetter = c
r.Reset()
fmt.Println(c.Count)
}
実行結果(コンパイルエラー):
# command-line-arguments
./main.go:15:19: cannot use c (variable of struct type Counter) as Resetter value in variable declaration: Counter does not implement Resetter (method Reset has pointer receiver)
メッセージの最後の method Reset has pointer receiver が理由を教えてくれています。Reset はポインタレシーバで定義されているので、値の Counter は Resetter を満たしません。
15行目を var r Resetter = &c に変えると通ります。
// 直す前
var r Resetter = c // Counter does not implement Resetter (method Reset has pointer receiver)
// 直した後
var r Resetter = &c
直した後の実行結果:
0
Reset が呼び出し元の c に反映され、Count が 0 になっています。
メソッドセットの規則
ある型が持つメソッドの集まりを、メソッドセットと呼びます。Go公式FAQでは「型 T のメソッドセットはレシーバが T のメソッドすべて、ポインタ型 *T のメソッドセットはレシーバが *T または T のメソッドすべて」と説明されています。
| 変数の型 | 値レシーバのメソッド | ポインタレシーバのメソッド |
|---|---|---|
T(値) |
含まれる | 含まれない |
*T(ポインタ) |
含まれる | 含まれる |
理由もFAQに書かれています。interface の中にポインタが入っていれば、そこから値を取り出して値レシーバのメソッドを呼べます。一方、interface の中に値が入っている場合、その値のアドレスを安全に得る方法がないため、ポインタレシーバのメソッドは呼べません。
先ほどの「c.AddPointer() は読み替えで呼べる」という話と混ざりやすいので、分けて覚えます。
| 場面 | ポインタレシーバのメソッドを値から使えるか |
|---|---|
変数から直接呼ぶ(c.Add()) |
使える(コンパイラが (&c).Add() に読み替える) |
interface に代入する(var r Resetter = c) |
使えない(メソッドセットに含まれない) |
値レシーバ・ポインタレシーバとinterfaceの組み合わせを動かす
package main
import "fmt"
type Labeler interface{ Label() string }
// 値レシーバで定義した型
type Tag struct{ Name string }
func (t Tag) Label() string { return "#" + t.Name }
// ポインタレシーバで定義した型
type Badge struct{ Name string }
func (b *Badge) Label() string { return "[" + b.Name + "]" }
// String() をポインタレシーバで定義した型
type Temp struct{ C float64 }
func (t *Temp) String() string { return fmt.Sprintf("%.1f度", t.C) }
func main() {
// 値レシーバのメソッドは、値でもポインタでも interface に入る
var l Labeler = Tag{"go"}
fmt.Println(l.Label())
l = &Tag{"struct"}
fmt.Println(l.Label())
// ポインタレシーバのメソッドは、ポインタだけが interface に入る
l = &Badge{"new"}
fmt.Println(l.Label())
// fmt.Stringer も同じ規則:値を渡すと String() は使われない
t := Temp{21.5}
fmt.Println(t)
fmt.Println(&t)
// メソッドは値として取り出して、あとで呼ぶこともできる
f := l.Label
fmt.Println(f())
}
実行結果:
#go
#struct
[new]
{21.5}
21.5度
[new]
| 出力 | わかること |
|---|---|
#go、#struct |
値レシーバで定義した Tag は、値でもポインタでも Labeler に入る |
[new] |
ポインタレシーバで定義した Badge は、&Badge{} を入れる |
{21.5} |
String() がポインタレシーバだと、値を Println に渡しても String() は使われない |
21.5度 |
&t を渡すと String() が使われる |
[new](最後) |
l.Label のようにメソッドを値として取り出し、あとで呼べる |
fmt.Println は、渡された値が fmt.Stringer(String() string を持つ interface)を満たすかを調べて表示を切り替えます。ここでもメソッドセットの規則がそのまま働くので、値のまま渡して表示を変えたい型は、String() を値レシーバで定義します。
埋め込みで構造体を組み合わせる
Goには継承がなく、代わりに「埋め込み」で型を組み合わせます。 フィールド名を書かずに型名だけを書くと、その型のフィールドとメソッドを外側の構造体から直接使えるようになります(昇格と呼びます)。
package main
import "fmt"
type Timestamps struct {
CreatedBy string
UpdatedBy string
}
func (t *Timestamps) Touch(user string) { t.UpdatedBy = user }
func (t Timestamps) Summary() string {
return "作成:" + t.CreatedBy + " 更新:" + t.UpdatedBy
}
type Article struct {
Timestamps // 埋め込み(フィールド名を書かない)
Title string
}
type Task struct {
Timestamps
Title string
}
// 同じ名前のメソッドを外側で定義すると、外側が優先される
func (t Task) Summary() string {
return "タスク「" + t.Title + "」 " + t.Timestamps.Summary()
}
type Summarizer interface{ Summary() string }
func main() {
a := Article{
Timestamps: Timestamps{CreatedBy: "佐藤"},
Title: "Goの構造体",
}
a.Touch("鈴木") // a.Timestamps.Touch("鈴木") と同じ
fmt.Println(a.UpdatedBy) // 昇格したフィールド
fmt.Println(a.Summary()) // 昇格したメソッド
fmt.Printf("%+v\n", a)
t := Task{Timestamps: Timestamps{CreatedBy: "高橋"}, Title: "レビュー"}
fmt.Println(t.Summary())
// 埋め込みで昇格したメソッドでも interface を満たせる
for _, s := range []Summarizer{a, t} {
fmt.Println("->", s.Summary())
}
}
実行結果:
鈴木
作成:佐藤 更新:鈴木
{Timestamps:{CreatedBy:佐藤 UpdatedBy:鈴木} Title:Goの構造体}
タスク「レビュー」 作成:高橋 更新:
-> 作成:佐藤 更新:鈴木
-> タスク「レビュー」 作成:高橋 更新:
| 出力 | わかること |
|---|---|
鈴木 |
a.Touch("鈴木") は a.Timestamps.Touch("鈴木") と同じ。a.UpdatedBy で埋め込んだ型のフィールドを直接読める |
作成:佐藤 更新:鈴木 |
埋め込んだ型のメソッド Summary() を a.Summary() で呼べる |
{Timestamps:{...} Title:Goの構造体} |
中身は「Timestamps という名前のフィールド」として入っている |
タスク「レビュー」 ... |
外側に同じ名前のメソッドがあると外側が優先。内側は t.Timestamps.Summary() で呼べる |
-> ... の2行 |
昇格したメソッドでも interface を満たせる |
埋め込みは「持っている」関係
埋め込みは便利ですが、他の言語の継承とは動きが違います。
| 比べる点 | 埋め込み |
|---|---|
| 関係 | Article は Timestamps を「持っている」。Article が Timestamps の一種になるわけではない |
| 代入 | var t Timestamps = a は書けない。a.Timestamps を取り出して渡す |
| 初期化 | Article{Timestamps: Timestamps{...}, Title: "..."} と、型名をフィールド名として書く |
| 同名のメソッド | 外側が優先される。内側のメソッドから外側のメソッドが呼ばれることはない |
| メソッドセット | 値で埋め込むと、外側の値のメソッドセットには値レシーバのメソッドだけが入る。外側のポインタにはポインタレシーバのメソッドも入る |
上のコードで []Summarizer{a, t} と値のまま入れられるのは、Summary() が値レシーバだからです。ポインタレシーバの Touch を求める interface であれば、&a を渡します。
どちらのレシーバを選ぶか:判断表
迷ったらポインタレシーバ、そして1つの型の中ではどちらかにそろえるのが基本です。Go公式FAQは、まず「メソッドがレシーバを変更する必要があるか」を確かめ、変更するならポインタにすること、次に「一貫性」を挙げ、一部のメソッドがポインタレシーバならほかもそろえることをすすめています。
| 確認すること | はい | いいえ |
|---|---|---|
| 1. メソッドがフィールドを書き換えるか | ポインタレシーバ | 2へ |
| 2. 同じ型のほかのメソッドにポインタレシーバがあるか | ポインタレシーバにそろえる | 3へ |
| 3. 構造体が大きい(フィールドが多い、配列を持つ)か | ポインタレシーバ(コピーを避ける) | 4へ |
4. sync.Mutex などコピーしてはいけないフィールドを持つか |
ポインタレシーバ | 5へ |
| 5. 小さくて変更しない値(座標、期間、色など)か | 値レシーバ | ポインタレシーバ |
型の種類ごとの目安も置いておきます。
| 型の例 | おすすめ | 理由 |
|---|---|---|
| 状態を持つもの(カウンタ、キャッシュ、接続、設定を更新するもの) | ポインタレシーバ | 変更を反映する必要がある |
| フィールドが多いデータ(会員、注文、記事など) | ポインタレシーバ | コピーが大きく、更新メソッドも持ちやすい |
小さな値(Point{X, Y}、Size{W, H}) |
値レシーバ | コピーが軽く、変更しない |
スライスやmapを元にした型(type IDs []int) |
読むだけなら値レシーバ | 中身は共有される。長さを変えるメソッドはポインタレシーバにする |
値のまま fmt.Println で表示を変えたい型 |
String() は値レシーバ |
値でもポインタでも Stringer を満たせる |
値レシーバとポインタレシーバを1つの型に混ぜると、「値では interface を満たせず、ポインタなら満たせる」という状態が生まれやすくなります。 そろえておけば、その型を値で扱うかポインタで扱うかも自然に決まります。ポインタレシーバにそろえた型は、コンストラクタ関数で *T を返し、スライスも []*T、mapも map[K]*T で持つと、この記事で見たコンパイルエラーに出会いにくくなります。
つまずきやすい点のチェックリスト
| 症状 | 原因 | 対応 |
|---|---|---|
| メソッドで変更したのに反映されない | 値レシーバでコピーを書き換えている | ポインタレシーバにする |
does not implement ... (method X has pointer receiver) |
値を interface に入れている | &v を渡す |
cannot call pointer method X on T |
mapの値やリテラルから直接呼んでいる | 変数に入れる、または map[K]*T にする |
for _, v := range list の中の変更が残らない |
v は要素のコピー | list[i].Method() と添字で呼ぶ |
invalid memory address or nil pointer dereference |
nil のポインタでフィールドに触れた | &T{} やコンストラクタ関数で作ってから使う |
too few values in struct literal |
順番指定のリテラルでフィールドが足りない | フィールド名つきで書く |
Println で String() が使われない |
String() がポインタレシーバで、値を渡している |
String() を値レシーバにするか &v を渡す |
assignment to entry in nil map |
構造体のmapフィールドを作っていない | コンストラクタ関数の中で make する |
よくある質問
Q. 値レシーバとポインタレシーバ、迷ったらどちらにしますか? A. ポインタレシーバにします。変更が反映され、コピーも起きず、あとから更新メソッドを足しても型全体の書き方を変えずに済みます。値レシーバが合うのは、座標のように小さくて変更しない値です。
Q. 構造体は関数に値で渡すのとポインタで渡すのと、どちらがよいですか?
A. 考え方はレシーバと同じです。関数の中で書き換えるならポインタ、読むだけで小さい構造体なら値で渡します。その型のメソッドがポインタレシーバにそろっているなら、関数の引数も *T にそろえると読みやすくなります。
Q. newと&T{}はどちらを使いますか?
A. 構造体では &T{...} をよく使います。初期値を一緒に書けるためです。new(T) はゼロ値のポインタを作るだけなので、初期値が要らないときに使えます。得られる型はどちらも *T です。
Q. 埋め込みと普通のフィールドは、どう使い分けますか?
A. 外側の型から a.Touch() のように直接呼びたい共通の機能(作成者・更新者の記録など)は埋め込み、単に部品として持つだけなら Owner Member のように名前つきのフィールドにします。名前つきにすると a.Owner.Name と書くことになり、どこの値かがコード上ではっきりします。
まとめ
- 構造体の初期化はフィールド名つきリテラルが基本。入力の確認や初期値の設定は
NewXxxのコンストラクタ関数にまとめる - 値レシーバはコピーに対して動くので、フィールドを書き換えても呼び出し元に反映されない
- ポインタレシーバは呼び出し元そのものに対して動く。変更するメソッドはポインタレシーバで書く
- メソッドセットの規則:値
Tには値レシーバのメソッドだけ、ポインタ*Tには両方が含まれる - ポインタレシーバのメソッドを持つ型は
&vを interface に入れる。値を入れるとmethod X has pointer receiverのコンパイルエラー - 埋め込みは継承ではなく「持っている」関係。フィールドとメソッドが昇格し、同名なら外側が優先
- 迷ったらポインタレシーバ、1つの型の中ではそろえる
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Go構造体とメソッドの書き方|値レシーバとポインタレシーバの違い」
株式会社RJCでは、Goの構造体とメソッドの書き方を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。