社内リポのディスパッチャの Handler 構造体を読んでいて、func (h *Handler) DispatchImport(...) の * の意味が分からなかった。調べて分かったのは、* あり/なしは気分ではなく 「本体を変更できるか」「コピーコスト」「型内統一」の3軸 で決まっているということ。
値レシーバー(*なし) | ポインタレシーバー(*あり) | |
|---|---|---|
| 渡るもの | 構造体の コピー | 本体の 住所 |
| 中身を書き換えると | コピーが変わって捨てられる(本体は不変) | 本体に反映される |
| 呼び出しコスト | 毎回まるごと複製 | アドレス1個分だけ |
Counter で count++ を3回する実験で確かめた。
count++ しても本物の count は 0 のまま逆に「* あり = どこかで変更している」は成り立たない。DispatchImport は h を読むだけなのに *Handler だ。* を付ける理由は変更だけでなく、コピー代の節約と型内統一(Goは1つの型でレシーバーを混在させない)もあるから。
type User struct {
Name string
Age int
}
func (u *User) Update() { u.Name = "hoge" } // *あり → 本体が変わる
func (u User) UpdateCopy() { u.Name = "hoge" } // *なし → コピーが変わって捨てられる
func main() {
u := &User{}
u.Update()
fmt.Printf("%+v\n", u) // &{Name:hoge Age:0}
u2 := &User{}
u2.UpdateCopy()
fmt.Printf("%+v\n", u2) // &{Name: Age:0} ← 変わらない
}
u := User{} でも u.Update() は動く。Go が自動で (&u).Update() に変換してくれる(変数のときだけ)無言のバグの温床:値レシーバーで中身を書き換えるコードはコンパイルが通ってしまう。「変更したはずなのに反映されない」ときは、まずレシーバーの * の有無を疑う。
おまけ:%w は fmt.Errorf 専用。Printf で使うと %!w(...) とエラー表示になる。構造体の中身を見たいときは %+v。
迷ったら3軸で考える:変更するか? コピーが重いか? 型内で統一されているか? 読むだけの型でもコピー代節約で * は普通にある。* の有無から「この型は変更される可能性があるか」を逆算できる。