Goのポインタをどう扱うべきか
Goのポインタについて、結局どうやって使い分けたらいいんだっけ?となることがあったので、挙動を整理したうえで使い所をまとめてみました。
ポインタの挙動
基本的な挙動
Goのポインタは、変数の値が格納されているメモリアドレスを指します。そのためポインタをPrintすると、その変数のメモリアドレスが表示されます。
func main() {
v := "Hello, World!" // v is a string
p := &v // p is a pointer to v
fmt.Println(v)
fmt.Println(p)
}
Hello, World!
0xc000086050
ポインタ型はnilを受け取れるため、ポインタ型の変数を宣言するとnilが代入されます。ポインタでない変数は宣言時に初期化されます。
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)
}
0
<nil>
プログラム側で同名の変数を上書きすると、ポインタが指すメモリアドレスは変更されず、新しい値が格納されます。
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)
}
Hello, World!
0xc0000140b0
---------
Hello, Go!
0xc0000140b0
新しい変数名を定義し、そこに定義済みの変数を代入すると、新たにメモリアドレスが割り当てられます。
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)
}
Hello, World!
0xc000086050
Hello, World!
0xc000086070
構造体とポインタ
構造体のフィールドにポインタ型を使うと、そのフィールドはポインタ型になります。
package pkg
type Item struct {
id int
name *string
}
func NewItem() Item {
name := "item1"
return Item{
id: 1,
name: &name,
}
}
func main() {
item := pkg.NewItem()
fmt.Printf("%#v\n", item)
}
pkg.Item{id:1, name:(*string)(0xc000104050)}
レシーバに取るメソッドを定義する場合、ポインタ型を使うと、そのメソッドはポインタ型のレシーバを受け取ります。
呼び出し元、レシーバはどの組み合わせでも問題なく呼び出せます。一つの構造体が値レシーバ、ポインタレシーバの両方を持つことができます。
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
}
func main() {
itemV := pkg.NewItem()
itemV.SetNameV("item2")
itemV.SetNameP("item3")
itemP := &itemV
itemP.SetNameV("item4")
itemP.SetNameP("item5")
}
値レシーバの場合、メソッド内で構造体のフィールドを変更しても、元の構造体のフィールドが持つ値は変更されません。
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
}
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)
}
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