Golang での関数引数 - 値渡しとポインタ渡しの指針
Golang
Lastmod: 2020-10-19

Golangでの値渡しとポインタ渡しの指針

Golang にはポインタの概念がある。なので、C/C++同様に関数引数型を値型・ポインタ型の2つが指定できる。

func print(s string) {
	fmt.Println(s)
}

func printPtr(s *string) {
	fmt.Println(*s)
}

値型で引数を渡した場合、関数スタックフレーム領域に引数値がコピーされる。一方、ポインタ型の場合は参照値のみがコピーされるのみで、実体となる値はヒープ上に存在する。

なお、ある関数のローカル変数を別の関数にポインタ渡しすると、その値はヒープ領域へエスケープされる。

code
package main

import "fmt"

type Product struct {
    name string
    price string
}

func main() {
	p := Product{}
    printProductPointer(&p)
}

func printProductPointer(p *Product) {
	fmt.Println(*p)
}
build with escape analysis
$ go build -gcflags "-m -l"
# github.com/rennnosuke/forblog/20200216
./func_arg.go:15:26: leaking param content: p
./func_arg.go:16:13: printProductPointer ... argument does not escape
./func_arg.go:16:14: *p escapes to heap

参考: Golang でのエスケープ処理とその解析

値/ポインタ渡しとエスケープ処理の声質を考慮した上で、関数の引数渡しの方針を個人的に決めてみた。なお、以下では基本型の値は特別な事情がない場合は値渡しするものとし、構造体型値をどう渡すかについて考える。

関数引数渡しの個人的方針

値渡しするとき

引数の構造体型が小さい場合

構造体のサイズが小さい場合、値コピーによるコストよりも、関数スタックフレーム領域からヒープ領域への割り当てコストが大きくなる。そのため、コピーコストを恐れて参照渡しするよりも素直にコピーしたほうがパフォーマンスに寄与する場合もある。また、値渡しのコピーによる副作用(ポインタ型プロパティをいじらない場合)回避の恩恵も受けられる。

Map, Slice, Channel などの参照型

型自体が実体となるデータへの参照+メタデータからなる型であり、この型の値そのもののサイズは肥大化しないので、基本値渡しする。

ポインタ渡しするとき

関数内で引数の値を変更する場合

sort 関数のように、仮引数値に対する変更を呼び出し元実引数値に反映したい場合は必然的にポインタ渡しになる。 個人的には副作用がある実装なので、あまり多用はしない。。

引数の構造体型が大きい場合

値渡しのコピーが無視できない場合。構造体のプロパティ値が多いときや、プロパティの型のサイズが大きい場合など。 ただし、ポインタ渡しにしたほうがよい構造体サイズの基準はないので、チーム内で規約として予め決めておくとやりやすいかもしれない。

関数呼び出しの回数が多い

実引数の呼び出し回数が多くなる、あるいはそれが予測できる場合は、コピーが頻発するため引数を参照渡しとしておく。あるいは実装内容によってはそもそも処理をインライン展開したほうが良いかもしれない。


とりあえず、現状は上記の基準で関数引数の型を決めている。 Java の名残で、以前は特別な理由がない限り構造体型はひたすらポインタ型で渡していたが、大きいサイズでなければ値渡しするようになってきた。

参考文献

Pass by pointer vs pass by value in Go