LoginSignup
7
4

More than 1 year has passed since last update.

【Golang】ポインタと実体の使いどころ

Posted at

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値を明確に区別したい時には、ポインタでの変数宣言が必要となる。

参考文献

技術評論社「Software Design (ソフトウェアデザイン) 2021年1月号」2020年12月18日

7
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
4