🚈

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

2023/12/19に公開

はじめに

goの学習を始めて躓いた部分について調査したので、今のところの自分の理解をまとめる。

ポインタの復習

  • メモリアドレスが格納されている変数
  • この住所を参照することによって変数の値を変更する(jsなどの参照渡しの概念に似てる気がする)
  • 変数からポインタを受け取るには"&"を変数の前につける
func main() {
	p := 1
	fmt.Println(p, &p)
}
// 1 0xc00000a158 
  • 変数への代入は単純な値コピーとなり、異なるメモリ領域にコピーが作られる
  • 代入後に元の値を変更してもコピー先には反映されない
func main() {
   // 値型の例
	p := 1
	q := p 
	q = 10
	fmt.Printf("pの値: %v\n", p)
	fmt.Printf("pのアドレス: %v\n", &p)

	fmt.Printf("qの値: %v\n", q)
	fmt.Printf("qのアドレス: %v\n", &q)
}
// pの値: 1
// pのアドレス: 0xc00000a158
// qの値: 10
// qのアドレス: 0xc00000a180
  • 変数には値型とポインタ型がある
    • 値型:
      • 変数が直接データを保持する
      • 変数に代入した場合は値をコピー
    • ポインタ型
      • 値のメモリアドレスを保持する
      • 変数に代入したらアドレスをコピー
func main() {
    // 値型の例
    i := 10
    j := i // iの値がjにコピーされる
    j = 20
    fmt.Println(i) // 10: iの値は変わらない

    // ポインタ型の例
    i = 10
    p := &i // pはiのアドレスを保持
    *p = 20 // pが指す値(つまりi)を変更
    fmt.Println(i) // 20: iの値が変わる
}

メソッドとは

特定の型に紐づけられた変数。
その型の変数に対して操作を行うことができる。

func (レシーバ レシーバの型) メソッド名(引数 引数の型) 戻り値の型 {
    // 処理
}

この例では型Userに対してメソッドを定義し、User型の変数から呼んでいる。
goにはクラスがないが、クラスと同じ感覚で理解できる

// 自己紹介するメソッド
func (h User) introduce() {
	fmt.Printf("I am %v years old\n", h.age)
	fmt.Printf("h.ageのアドレス: %v\n", &(h.age))
}
func main() {
	u := User{
		ID:   1,
		Name: "Taro",
		age:  20,
	}
	u.introduce()
}
// I am 20 years old

レシーバについて

  • 値型に対してメソッドを定義するか、ポインタ型に対してメソッドを定義するのかの2種類がある。

値型レシーバ

値型のレシーバにした場合、メソッドが呼び出される度に新しい構造体にコピーされる。
そのため、値レシーバのメソッド内でフィールドを変更した後に参照しても変更が反映されない。

// 自己紹介するメソッド
func (h User) introduce() {
	fmt.Printf("I am %v years old\n", h.age)
	fmt.Printf("h.ageのアドレス: %v\n", &(h.age))
}

// 年齢を1つ増やすメソッド
func (h User) setAge(age int) { //値レシーバ
	h.age = age
}

func main() {
	u := User{
		ID:   1,
		Name: "Taro",
		age:  20,
	}
	u.introduce()
	u.setAge(30)
	u.introduce()
}

// アドレスが異なるので、変更が反映されない
// I am 20 years old
// h.ageのアドレス: 0xc000064498
// I am 20 years old
// h.ageのアドレス: 0xc0000644d8

ポインタ型レシーバ

レシーバをポインタにすることで、メソッドからレシーバの値を直接変更できる

// 自己紹介するメソッド
func (h User) introduce() {
	fmt.Printf("I am %v years old\n", h.age)
	fmt.Printf("h.ageのアドレス: %v\n", &(h.age))
}

// 年齢を1つ増やすメソッド
func (h *User) setAge(age int) { //ポインタレシーバ
	h.age = age
}

func main() {
	u := User{
		ID:   1,
		Name: "Taro",
		age:  20,
	}
	u.introduce()
	u.setAge(30)
	u.introduce()
}

// アドレスが同じなので変更が反映される
// I am 20 years old
// h.ageのアドレス: 0xc000064498
// I am 30 years old
// h.ageのアドレス: 0xc000064498

まとめ

値レシーバ:

  • メソッドが呼び出されるたびに新しいコピーが作成される
  • メソッド内で変更しても元のフィールドには反映されない

ポインタレシーバ

  • ポインタを受け取るので、メソッド内の変更が反映される。
  • メモリ上のアドレスを渡すだけなので、メモリを食わない

基本的には値を変更したくてもいいときは値レシーバ、それ以外ではポインタレシーバって覚えればいいかな。

Discussion