はじめに
Goでコードを書いていると、必ず一度は悩む問題があります。
func foo(u User) {}
func foo(u *User) {}
そう、関数の引数の構造体をポインタ型にすべきかどうかです。
私も説明しろと言われて「ウッ」となってしまったので整理します!
Goにおける値渡しとポインタ渡し
関数の引数の構造体をポインタ型にすべきかどうかを議論する前に、Goにおける値渡しとポインタ渡しについて整理します。
値渡し
func foo(u User) {}
関数呼び出し時に 構造体のコピー が作られます。
そのため、関数内で値を変更しても呼び出し元には影響しません。
ポインタ渡し
func foo(u *User) {}
呼び出し元と同じ実体を引数に渡しています。
そのため、関数内の変更は呼び出し元にも反映されます。
関数の引数の構造体をポインタ型する判断基準
以下のいずれかに当てはまったらポインタを使うのが良いと思います
- 構造体を変更する
- 構造体が大きい
- 構造体がnilの場合を表現したい
順番に見ていきます。
構造体を変更する
NG例(値渡し)
func UpdateName(u User) {
u.Name = "Alice"
}
構造体が値渡しになっているため、呼び出し元の構造体は変更されません。
関数内で構造体の中身を変更し、返す場合はポインタ1択となります。
OK例(ポインタ)
func UpdateName(u *User) {
u.Name = "Alice"
}
構造体が大きい
値渡しは構造体のコピーを生成するため、構造体の中身のデータ量が多い場合はコピーをする分、余計に容量を食ってしまいます。
そのためクソデカ構造体や、中身にバイナリデータを含むなどの容量が大きい構造体を扱うときはポインタ型で渡しましょう
構造体がnilの場合を表現したい
値型の構造体は初期化しても必ず値を持ちます。
var u User // 何らかのゼロ値が入る。nilではない
ポインタ型であれば下記のようにnilで初期化することができます。
var u *User = nil
nilの状態を使いたい、初期化したい場合はポインタ型を使うのが良いかと思います。
ほな全部ポインタ型でええんちゃうか!
パフォーマンスの面や値渡しによる懸念があるので全部ポインタ型にしたくなりますが、デメリットもあります。
nilチェックが爆発する
先述の通りポインタ型は nil を取ることができます。
当然処理によってはnilだとpanicになることがあります。下記がその例です。
func PrintName(u *User) {
fmt.Println(u.Name) // uがnilの場合はpanic!!
}
そのため、事前にnilチェックを実装することになります。
func PrintName(u *User) {
// nilでないことをチェックする
if u == nil {
return
}
fmt.Println(u.Name)
}
nilチェックさえ実装すれば通常通り使えますが、明らかにnilを取ることがない場合は不要な処理になります。
そのため、何にでもポインタ型、というのは不適切であることが分かります。
読み取り専用なのに変更できそうに見える
ポインタ渡しは呼び出し元と同じ実体を渡すので、値の更新などで利用するのが適しています。
しかし、読み取り専用の関数(引数構造体の中身を変更しない関数)であるにも関わらず、ポインタ型で引数を指定してしまうと、この関数内で変更する?とも読み取れてしまいます。
まとめ
基本スタンスとしては値渡しで実装し、必要な時のみポインタ型にするようにしましょう!
おわりに
違いがよく分かっていなかった頃の私は「とりあえず既存の実装参考にすれば良いか!」くらいに思っていた&そこまで指摘されることはなかったので、あんまり困ったことはなかったです。
が、説明する立場になって改めてどういう違いがあるのかをしっかり理解しておかねば…と思い整理してみました!
最後まで読んでいただきありがとうございます!