imu-note
いむたろ
いむたろ
@imutaroh
新卒エンジニア / データ基盤 × AI

What — 2種類のレシーバー

社内リポのディスパッチャの Handler 構造体を読んでいて、func (h *Handler) DispatchImport(...)* の意味が分からなかった。調べて分かったのは、* あり/なしは気分ではなく 「本体を変更できるか」「コピーコスト」「型内統一」の3軸 で決まっているということ。

値レシーバー(*なし)ポインタレシーバー(*あり)
渡るもの構造体の コピー本体の 住所
中身を書き換えるとコピーが変わって捨てられる(本体は不変)本体に反映される
呼び出しコスト毎回まるごと複製アドレス1個分だけ

Counter で count++ を3回する実験で確かめた。

  • 値レシーバー:メソッド内で見える内部アドレスが毎回変わる(呼ぶたびに複製されている証拠)。3回 count++ しても本物の count は 0 のまま
  • ポインタレシーバー:常に本体と同じアドレス。count はちゃんと 3 になる

逆に「* あり = どこかで変更している」は成り立たない。DispatchImport は h を読むだけなのに *Handler だ。* を付ける理由は変更だけでなく、コピー代の節約と型内統一Goは1つの型でレシーバーを混在させない)もあるから。

How — 書き換えが効く/効かないを見分ける

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() に変換してくれる(変数のときだけ
Warning

無言のバグの温床:値レシーバーで中身を書き換えるコードはコンパイルが通ってしまう。「変更したはずなのに反映されない」ときは、まずレシーバーの * の有無を疑う。

おまけ:%wfmt.Errorf 専用。Printf で使うと %!w(...) とエラー表示になる。構造体の中身を見たいときは %+v

Success

迷ったら3軸で考える:変更するか? コピーが重いか? 型内で統一されているか? 読むだけの型でもコピー代節約で * は普通にある。* の有無から「この型は変更される可能性があるか」を逆算できる。

関連