🏓

【Go】値レシーバとポインタレシーバについて

2023/01/19に公開

はじめに

Goでメソッドを宣言する際、レシーバには値レシーバとポインタレシーバがあります。

この記事では、この2つのレシーバの挙動について検証しています。


値レシーバの挙動

値レシーバの挙動については、つぎのコードを例に確認します。

package main

import "fmt"

type Human struct {
	Age int
}

func (h Human) PrintAge() {
	fmt.Println(h.Age)
}

func (h Human) SetAge(age int) {
	h.Age = age
}

func main() {
	h := &Human{Age: 20}
	h.PrintAge() // => 20
	h.SetAge(30)
	h.PrintAge() // => 20
}

このコードでは、h.SetAge(30)を呼んだ後にh.PrintAge()を実行しても、出力される値は20のままです。
構造体の値を変更するメソッドを実行しても値が変わらないという場合、値レシーバが使われているのが原因だったということは、よくある話です。

それでは、なぜ値レシーバを使用すると、値が変わらないのかという理由について確認します。
上記のコードに、h.Ageのアドレスを出力する処理を追加しました。

func (h Human) PrintAge() {
	fmt.Println(h.Age)
	fmt.Println(&h.Age) // (1)
}

func (h Human) SetAge(age int) {
	h.Age = age
	fmt.Println(&h.Age) // (2)
}

実行結果はつぎのとおりです。

; (1)の出力
0x1400001a088
; (2)の出力
0x1400001a098

この内容を見るとわかるように、値レシーバで定義したメソッドのh.Ageは、それぞれアドレスが異なっています。値レシーバは実行するたびに構造体がコピーされ、参照しているアドレスが変化します。
そのため、値レシーバを通して構造体のデータを変更しようとしても、値が変わらないという状況が発生します。

値レシーバの注意点

ポインタの変数を持つ構造体の場合、注意が必要です。
値レシーバを使用する理由として、イミュータブルにしておきたいということが考えられますが、構造体が持つ変数がポインタの場合、値が変更できてしまうケースがあります。

サンプルコードをつぎに示します。

type Human struct {
	Age *int
}

func (h Human) PrintAge() {
	fmt.Println(*h.Age)
}

func (h Human) SetAge(age int) {
	*h.Age = age
}

func main() {
	age := 20
	h := &Human{Age: &age}
	h.PrintAge() // => 20
	h.SetAge(30)
	h.PrintAge() // => 30。値が変更できてしまっている
}

SetAge()の中でポインタの値を書き換えるようになっているため、このコードでは、Ageの値が変更されます。


ポインタレシーバの挙動

つぎのコードを例に、ポインタレシーバの挙動を確認します。

package main

import "fmt"

type Human struct {
	Age int
}

func (h *Human) PrintAge() {
	fmt.Println(h.Age)
}

func (h *Human) SetAge(age int) {
	h.Age = age
}

func main() {
	h := &Human{Age: 20}
	h.PrintAge() // => 20
	h.SetAge(30)
	h.PrintAge() // => 30
}

このコードは前述の値レシーバのときと違い、h.SetAge(30)の実行により、値が書き換わりました。
値レシーバの挙動を確認したときと同様に、処理を追加してアドレスがどうなっているか見てみます。

func (h *Human) PrintAge() {
	fmt.Println(h.Age)
	fmt.Println(&h.Age) // (1)
}

func (h *Human) SetAge(age int) {
	h.Age = age
	fmt.Println(&h.Age) // (2)
}
; (1)の出力
0x1400001a088
; (2)の出力
0x1400001a088

ポインタレシーバの場合は、メソッド呼び出し時に構造体はコピーされず、同じポインタが使用されるため、値の変更が反映されます。

Discussion