Goのポインタをどう扱うべきか

2024/10/19に公開

Goのポインタについて、結局どうやって使い分けたらいいんだっけ?となることがあったので、挙動を整理したうえで使い所をまとめてみました。

ポインタの挙動

基本的な挙動

Goのポインタは、変数の値が格納されているメモリアドレスを指します。そのためポインタをPrintすると、その変数のメモリアドレスが表示されます。

main.go
func main() {
	v := "Hello, World!" // v is a string
	p := &v              // p is a pointer to v

	fmt.Println(v)
	fmt.Println(p)
}
output
Hello, World!
0xc000086050

ポインタ型はnilを受け取れるため、ポインタ型の変数を宣言するとnilが代入されます。ポインタでない変数は宣言時に初期化されます。

main.go
func main() {
	var v int  // v is an int
	var p *int // p is a pointer to an int

	fmt.Println(v)
	fmt.Println(p)
}
output
0
<nil>

プログラム側で同名の変数を上書きすると、ポインタが指すメモリアドレスは変更されず、新しい値が格納されます。

main.go
func main() {
	v := "Hello, World!" // v is a string
	p := &v              // p is a pointer to v

	fmt.Println(v)
	fmt.Println(p)

	v = "Hello, Go!"
	fmt.Println(v)
	fmt.Println(p)
}
output
Hello, World!
0xc0000140b0
---------
Hello, Go!
0xc0000140b0

新しい変数名を定義し、そこに定義済みの変数を代入すると、新たにメモリアドレスが割り当てられます。

main.go
func main() {
	v := "Hello, World!" // v is a string
	p := &v              // p is a pointer to v

	fmt.Println(v)
	fmt.Println(p)

	v2 := v
	p = &v2

	fmt.Println(v2)
	fmt.Println(p)
}
output
Hello, World!
0xc000086050
Hello, World!
0xc000086070

構造体とポインタ

構造体のフィールドにポインタ型を使うと、そのフィールドはポインタ型になります。

pkg/item.go
package pkg

type Item struct {
	id   int
	name *string
}

func NewItem() Item {
	name := "item1"
	return Item{
		id:   1,
		name: &name,
	}
}
main.go
func main() {
	item := pkg.NewItem()
	fmt.Printf("%#v\n", item)
}
output
pkg.Item{id:1, name:(*string)(0xc000104050)}

レシーバに取るメソッドを定義する場合、ポインタ型を使うと、そのメソッドはポインタ型のレシーバを受け取ります。
呼び出し元、レシーバはどの組み合わせでも問題なく呼び出せます。一つの構造体が値レシーバ、ポインタレシーバの両方を持つことができます。

pkg/item.go
package pkg

type Item struct {
	id   int
	name string
}

func NewItem() Item {
	name := "item1"
	return Item{
		id:   1,
		name: name,
	}
}

func (i Item) SetNameV(name string) *Item {
	i.name = name
	return &i
}

func (i *Item) SetNameP(name string) *Item {
	i.name = name
	return i
}
main.go
func main() {
	itemV := pkg.NewItem()
	itemV.SetNameV("item2")
	itemV.SetNameP("item3")

	itemP := &itemV
	itemP.SetNameV("item4")
	itemP.SetNameP("item5")
}

値レシーバの場合、メソッド内で構造体のフィールドを変更しても、元の構造体のフィールドが持つ値は変更されません。

pkg/item.go
package pkg

type Item struct {
	id   int
	name string
}

func NewItem() Item {
	name := "item1"
	return Item{
		id:   1,
		name: name,
	}
}

func (i Item) SetNameV(name string) *Item {
	i.name = name 
	return &i
}

func (i *Item) SetNameP(name string) *Item {
	i.name = name
	return i
}

main.go
func main() {
	item := pkg.NewItem()
	fmt.Printf("before %#v\n", item)
	item.SetNameV("item2")
	fmt.Printf("after  %#v\n", item)

	item = pkg.NewItem()
	fmt.Printf("before %#v\n", item)
	item.SetNameP("item2")
	fmt.Printf("after  %#v\n", item)
}
output
before pkg.Item{id:1, name:"item1"}
after  pkg.Item{id:1, name:"item1"}
before pkg.Item{id:1, name:"item1"}
after  pkg.Item{id:1, name:"item2"}

ポインタをどう使うか

DDD, クリーンアーキテクチャの観点から、ポインタをどう使うかを考えてみます。

アプリケーションの中でポインタとしてオブジェクトを扱うかどうかは、そのオブジェクトの同一性をどのように判定するかに依存しています。

DDDでいうEntityは、同一性をIDで判定します。このIDが同じであれば同じオブジェクトとして扱いますが、Entityオブジェクトはアプリケーションの中で一意である必要があります()。そのため、Entityオブジェクトはポインタ型で扱い、メモリアドレスを以て同一性を判定するのが適しています。

それに対して、ValueObjectは値が等しいかどうかで判定します。そのため、ValueObjectオブジェクトは値型で扱い、値が等しいかどうかで同一性を判定するのが適しています。

Entityはポインタで扱う。Value Objectは値で扱う

そしてレシーバについてですが、こちらは制限を設けることが難しいですが、お気持ち程度にはなりますが、以下のような考え方があるかと思います。

Entityオブジェクトは、メソッドによって状態を変更することがドメインロジックとして重要な役割であるため、ポインタレシーバを使うことが適しています。

ValueObjectオブジェクトは、メソッドによって状態を変更する必要がなく、常に新しいオブジェクトを生成することが適しています。そのため、値レシーバを使うことが適しています。

Entityオブジェクトであっても、オブジェクトの状態に変更を与えないというのを明示するために、値レシーバを使うこともあってよいかと思います。

Entityにはポインタレシーバ。Value Objectには値レシーバ

Discussion