Goで値渡しするか参照渡しするかの指針
Goでは値渡しと参照渡しが簡単に行なえます。
そのどちらにもメリット・デメリットがあって、どうすればいいんかなぁとたびたび悩むので、個人的な判断基準を作ってメモっておこうという趣旨で、この記事を書きます。
値渡し
まず値渡しについてのメモ。
変更されない
値渡しのメリットは変更されないこと。
渡す側は渡した変数が書き換えられることを恐れる必要はないし、
渡される側は変数を格納しても変更されることを恐れる必要がない。
その時点だけに注目すれば良いのが大きい。
元から中身がポインタの多い構造体は値渡しのコストが小さい
参照渡しのほうで書くけど、値渡しではコピーが発生する。
コピーは渡すものの大きさによってコストが変わるけど、渡すものが大きければ大きいほどコストも大きくなる。
ただ、外から見て大きく感じる構造体でも、実は中身はポインタってことがある。
例えばSliceとかMapがそう。
Sliceの中身はArrayのポインタが大半なので、コピーのコストが小さい。
参照渡し
つぎに参照渡しについてのメモ。
コピーされない
参照渡しのメリットはコピーされないこと。
大きな構造体を渡したりするとき、コピーすると大きなメモリを確保することになって複製にコストがかかる。
参照渡しであればあらたにメモリを確保する必要がないからコストを気にする必要がない。
nilが使える
参照渡しではnilが発生しうる。
func NewHoge(fuga, piyo string) (*Hoge, error) みたいな関数の場合、エラーが発生したらHogeをnilに出来る。
errorを正しくハンドリングするのは関数を利用する側の責務ではあるけど、ゼロ値の構造体を返すとそれを利用されてしまうかもしれないとか考えると、なるべく何も返さないという選択をしたくなる。
引数でも同じで、引数がポインタならnilを渡されることがある。
interfaceを求めるところに渡す場合は参照渡しになる
interfaceとしてやり取りしたい場合は参照渡しにするしかなくなる。
code:iface.go
func Request(req Requester) (Response, error) {
return req.Request()
}
type Requester interface {
Request() (Response, error)
}
type hogeRequester struct{}
func (r *hogeRequester) Request() (Response, error) {
var res Response
var err error
// ... なんらかの処理
return res, err
}
こんな感じで、Request関数にRequesterinterfaceを実装した構造体を渡す場合、参照渡しする必要がある。
コピーしないことで並列処理に使える・並列処理にリスクがある
例えばsync.Mutexという排他制御のための構造体がある。
これはコピーしてはいけない = 値渡ししてはいけない。
コピーされるとその時点での状態のまま複製されるから、Lockされたまま渡されたらどうにもできないし、どこかでUnlockが走ってもコピーされたものには反映されない。
つまり、並列処理をするうえで、sync.Mutexは絶対に参照渡しで渡す必要がある。
これだけだと *sync.Mutexって定義するしかないって誤解をうけるから補足しておくと、
sync.Mutexがグローバルな変数になってたり、sync.Mutexを持つ構造体が参照渡しされていればsycn.Mutexがポインタである必要はない。
code:mutex.go
var (
controller *Controller
mtx sync.Mutex
)
type Controller struct {
mtx sync.Mutex
}
func (c *Controller) Exec() error {
c.mtx.Lock
defer c.mtx.Unlock
// ... 何かしらの処理
return nil
}
func GetController() *Controller {
mtx.Lock()
defer mtx.Unlock()
if controller == nil {
controller = new(Controller)
}
return controller
}
逆に、他の並列処理の影響を受けたくないのであれば、参照渡しじゃなくて値渡ししておけば状態を変更されるリスクをおわなくてすむ。
参照渡しであるべきパターン
以上の条件から、ポインタのほうがいいのは、
nilを意味のあるものとして受けたい
例えば、boolのゼロ値はfalse。trueでもfalseでもない未設定を表現したい場合にnil許容な*boolにするっていうイメージ。boolをnil許容にする設計はどうなのかっていう意見もあると思うけどね!
渡さないということを表現できる
返り値がnil許容なら返さないというのも表現できる
interface
状態を共有したい
かなーと。
値渡しであるべきパターン
以上の条件から、値のほうがいいのは、
状態を変更されたくない
影響を受けたくない、独立性を保ちたい
nilを受けたくない
かなーと。
以上のどちらでもない場合はメモリ効率を意識する
参照渡しであるべきパターン、値渡しであるべきパターン以外でどうしようかなーと迷ったときはメモリ効率の良いほうを選べばいいと思う。
基本的には参照渡しになると思うけど、プリミティブな型であれば値渡しでもいいやろうし、小さな構造体であれば値渡しでコピーが発生しても問題ない場合も多い。
とはいえ、値渡しであるべきパターンでもメモリ効率が悪いから参照渡しを選択するという場合もある。
その辺は表現と効率の天秤で考える。
まとめ
参照渡しか値渡しかを考えるときに、interfaceとかnil許容が実装として必要かを考える必要はある。
メモリ効率だけを追及するのはモダンな開発にはあってないという考えなので、意味を表現すること、表現する上で必要な知識と技術は身に着けておいたほうが良い。
更新履歴