Golangにおけるポインタの使いどころ、実体の使いどころを整理しました。
結論
イミュータブル変数 | 関数引数/メソッドレシーバ | ゼロNULL区別 | |
---|---|---|---|
ポインタ | × | ◎ | ○ |
実体 | ○ | ○ | × |
1. イミュータブル変数は「実体」で宣言する。
ポインタは、別変数に値を渡すと、その変数からも元値の更新が可能となる。
つまり、複数の変数間で1つの値を共有するミュータブルな変数を意味する。
package main
import "fmt"
func main() {
v1 := &S{Number: 1} // ポインタで宣言
v2 := v1 // 別変数に値を渡す
v2.Number = 10 // 別変数を更新
fmt.Printf("v1: %v, v2: %v", v1, v2)
// -> v1: &{10}, v2: &{10}
// v1の値も更新されてしまう: ミュータブル変数
}
type S struct {
Number int
}
一方で、実体を別変数に渡した場合、元の実体からコピーされた別の値が生成される。
よって、値を管理するのはただ1つの変数だけとなるイミュータブル変数を意味する。
package main
import "fmt"
func main() {
v1 := S{Number: 1} // 実体で宣言
v2 := v1 // 別変数に値を渡す
v2.Number = 10 // 別変数を更新
fmt.Printf("v1: %v, v2: %v", v1, v2)
// -> v1: &{1}, v2: &{10}
// v1の値は更新されない: イミュータブル変数
}
type S struct {
Number int
}
2. パフォーマンスを向上させるには関数の引数またはメソッドのレシーバを「ポインタ」で宣言する。
関数を呼び出す際、引数は元の値をコピーして受け取るため、ポインタであればアドレス値(64ビットCPUであれば8バイト分の値)を引数として読み込むことになる。
package main
import (
"fmt"
"unsafe"
)
func main() {
s := &S{Number: 10000000, Name: "someone", Address: []byte("abcdefg")}
// ポインタを引数に渡す
SizeChecker(s)
// -> Param Size: 8 bytes
}
type S struct {
Number int
Name string
Address []byte
}
func SizeChecker(s *S) { // ポインタを引数として受け取る
fmt.Printf("Param Size: %v bytes", unsafe.Sizeof(s))
}
一方で、実体を引数に渡す場合、実体のデータそのものをコピーするため、(フィード数の多い構造体など)大きなデータ構造を持つ実体はコピーに必要なメモリ領域が大きくなり、パフォーマンス劣化の原因となる。
package main
import (
"fmt"
"unsafe"
)
func main() {
s := S{Number: 10000000, Name: "someone", Address: []byte("abcdefg")}
// 実体を引数に渡す
SizeChecker(s)
// -> Param Size: 48 bytes
// ポインタを渡す場合よりもコピーコストが増加: パフォーマンス劣化の原因
}
type S struct {
Number int
Name string
Address []byte
}
func SizeChecker(s S) { // 実体を引数として受け取る
fmt.Printf("Param Size: %v bytes", unsafe.Sizeof(s))
}
よって、関数のパフォーマンス向上を目的とした場合、引数はポインタで渡すほうが望ましい。
なお、構造体のメソッドも同様に、レシーバはポインタで宣言したほうがパフォーマンスを向上できる。
package main
import (
"fmt"
"unsafe"
)
func main() {
s := S{Number: 10000000, Name: "someone", Address: []byte("abcdefg")}
s.PointerSizeCheckMethod() // -> Param Size: 8 bytes
s.DataSizeCheckMethod() // -> Param Size: 48 bytes
}
type S struct {
Number int
Name string
Address []byte
}
func (s *S) PointerSizeCheckMethod() { // レシーバをポインタで受け取る
fmt.Printf("Param Size: %v bytes\n", unsafe.Sizeof(s))
}
func (s S) DataSizeCheckMethod() { // レシーバを実体で受け取る
fmt.Printf("Param Size: %v bytes\n", unsafe.Sizeof(s))
}
3. ゼロ値とNULL値の区別が必要なデータは「ポインタ」で宣言する。
ポインタで初期値を指定せずに変数宣言すると、初期値には<nil>が格納される。
これはアドレス値が存在しないこと(=NULL)を意味する。
package main
import "fmt"
func main() {
var id *int // ポインタで宣言
var name *string // ポインタで宣言
fmt.Printf("Init Value: id=%v, name=%v", id, name)
// -> Init Value: id=<nil>, name=<nil>
// ポインタの初期値は<nil>になる
}
一方、実体で初期値を指定せずに変数宣言した場合、初期値には各型で決められた値が格納される。
特に、int型では初期値にゼロが格納されるため、ゼロとNULLを区別できない。
package main
import "fmt"
func main() {
var id int // 実体で宣言
var name string // 実体で宣言
fmt.Printf("Init Value: id=%v, name=%v", id, name)
// -> Init Value: id=0, name=
// 実体の初期値は型ごとに異なる
// (int型: 初期値はゼロ, string型: 初期値は空文字)
}
よって、ゼロ値とNULL値を明確に区別したい時には、ポインタでの変数宣言が必要となる。