[Go] Pointer
目的
- 扱いを定めて記法を統一する
方針
原則的に値を用いる。
ただし、以下の場合にはポインタを用いる。
- ライブラリにてポインタで定義されている型
- コンストラクタの返り値
- レシーバ
- 数字型で存在しないこと(NULL)を表現したい場合
- structのembedding, 構造体をプロパティとして持たせる場合
- 関数内で引数で受け取ったarray, slice, structを変更したい場合
- 副作用が発生するので基本的には使わない
- mapに持たせる場合の構造体
- Request Parameterに対応する構造体の数値型のうち一部(後に詳細)
性能
以下の記事によると、値の方がパフォーマンス良いとのこと。
いくつかの記事を漁ってみたが、だいだい似たような検証結果が記載されていた。
しかし、引数、返り値、レシーバ、埋め込みなど様々なユースケースがあるので、一概に値の方がパフォーマンスが良いと断言はできないと思われる。
各方針の参考・例
コンストラクタの返り値
ポインタで返すのが一般的。
Effective Go - Constructors and composite literals
レシーバ
ポインタを使うのが一般的。
CodeReviewComments - Receiver Type
Choosing whether to use a value or pointer receiver on methods can be difficult, especially to new Go programmers. If in doubt, use a pointer
数字型で存在しないこと(NULL)を表現したい場合
例えば、RepositoryDTO/Entityのうち、プロパティに対応するDBのフィールドがNULL許容の場合など。
CREATE TABLE `hoge` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
`user_id` bigint unsigned DEFAULT NULL COMMENT 'ユーザーID',
PRIMARY KEY (`id`)
)
type Hoge struct {
ID uint
UserID *uint
}
ポインタにしない場合
ゼロ値で初期化される。
上記の場合で値にすると、UserIDは0で初期化され、本当に0の場合と区別がつかない。
ポインタにする対象の型
数字型(int, uint)のみとする。
極力ポインタを扱いたくなく、数字型以外の型でNULLを扱いたいシーンが想定されないためである。
仮に、NULL許容なstring型のフィールドがあったとしても、それはアプリ上では空文字""
と表現しても差し支えない。
NULL許容なboolean型は想定されづらい。
sql.NullXXXは使わない
実際に使っていないが、取り回しが悪いように思われる。
SQLのNULL値に対応する(JSON定義の変更まで)【Go】
structのembedding, 構造体をプロパティとして持たせる場合
ポインタを埋め込むのが一般的。
The embedded elements are pointers to structs and of course must be initialized to point to valid structs before they can be used.
ポインタにしない場合
埋め込んだ構造体がゼロ値で初期化されてしまうため、埋め込んだ構造体が数字型のプロパティを持っていると、前述のように本当に0かどうか判別できなくなってしまう。
関数内で引数で受け取ったarray, slice, map, structを変更したい場合
副作用が発生するので基本的には使わない。
やむを得えない場合にのみポインタ渡しとする。
mapに持たせる場合の構造体
mapに持たせる場合の構造体はポインタを使う。
Go言語 構造体のmapはポインタを使おう
Request Parameterに対応する構造体の数値型のうち一部
requiredタグが付与された数値型のうち、0が代入される可能性がある場合、ポインタを使用する。
値の場合、0が代入されるとvalidation errorとなる。
This validates that the value is not the data types default zero value. For numbers ensures value is not zero.
Error when put 0 for required int field
you should use a pointer to an integer instead of an integer itself.
参考
ポインタにしない場合に関する整理
引数
上述の通り、基本的に引数は値渡しとする。
上記のその他に掲載しているリンク先では、コピーのコストを考えて、大きい構造体を渡す場合にはポインタで渡すことを提唱している。
しかし、可読性と統一性を考慮して、値渡しで統一する。
ただし、シビアな性能要件が発生した場合には別途検討する。
array, slice
array, sliceの要素は値で持つ
ポインタで持つとメモリ効率が悪いため、値で持つ。
The no-pointer version allocates less memory, performs fewer allocations and is over an order of magnitude faster.
sliceの宣言
capacityを宣言しない
- メモリ効率は良くないが、著しく性能を下げるわけではない
- 実装の容易性を考慮してcapacityを宣言しない
- ただし、シビアな性能要件が発生した場合には別途検討する
- 参考
appendを前提とする場合
var
で宣言すると初期値はnilとなる。
宣言時に配列がアロケートされるのを防ぐ。
var a []int
a = append(a, 1)
宣言段階で空のスライスが生成されることが望ましい場合
Repositoryでスライスを返す関数などが想定される。
a := []int{}
a = append(a, 1)
map
mapはポインタなので、そのまま扱う。
# 正確にはmap構造体へのポインタ