【Golang】関数のレシーバータイプの決め方
2022年04月22日 14:13
こんにちは、ISSUEの寒河江です。
関数のレシーバーは値か、ポインターにするべきか明確に理解できていなかったので、公式ブログ等を参考に改めてまとめてみました。
まずは本題に入る前に簡単に2つのレシーバータイプについてご説明します。
レシーバーがポインターではないタイプです。下記Read
の関数のようなレシーバです。vはコピーされた構造体でgetterとして使われることが多いです。
レシーバーがポインターのタイプです。下記Write
関数のようなレシーバです。vはアドレス参照なのでv.dataの値にはstrがセットされます。値型では構造帯がコピーされているのでレシーバーの値が変わることはありません。このようなsetterとしての使われ方と、構造体をコピーしないので効率的なプログラムになります。
上記2つのレシーバータイプがあり、ここからこの2つの使い分けについて書いていきます!
レシーバータイプの分け方は値とポインターの特性の違いによると思いました。
ポインタータイプ→ポインタ渡しでオブジェクトの値を変更したい場合、またはレシーバーをコピーするコストが大きいとき
値→小さなオブジェクトでコピーコストが小さいとき、値の変更が必要ない時(読み取り専用)
やや例外はあるもののレシーバータイプの決め方は上記が基準になりそうです。
参照型データの場合は、値のままでも値を書き換えることができるのでポインター型にする必要がありません。
先ほどのsetterの話と同様です。値の場合、構造体のコピーが渡されてきているのでレシーバーの値を書き換えることはできません。下記PointerWrite
のようにレシーバーがポインターの場合、S構造体のdataの値を書き換えることができます。
出力
サンプル
標準ライブラリに組み込まれているsync.Mutexはコピーされてしまうと挙動がおかしくなります。下記valueMuxのようにsync.Mutex
を含むstructを値レしバーにするとgo vetでも指摘してくれます。
レシーバーの値が大きいものはポインターにして構造体をコピーするコストを抑えるのが効果的です。大きいの基準は引数に取った時に大きいと感じるものだそうです。大体の構造体は大きいと感じるので基本的にはレシーバーで問題なさそうですね。
先ほど値タイプとポインタータイプのレシーバの違いで説明した内容になります。ポインターレシーバーで呼び出した場合、レシーバーの値を変更することができます。値タイプの場合はレシーバをコピーしているのでレシーバーにstructの変更は反映されません。
出力
stringやint、小さな構造体、配列のようなプリミティブな値の場合は値レシーバーを利用します。理由は値レシーバーを関数内に渡す場合ヒープに割り当てる代わりにスタックのコピーを利用できるためガベージの量が減る可能性があるとのこと。
(この辺りはpospomeさんの記事を参考にしています。)
time.Time型は以下の理由から不変型です。
・フィールドがすべてプライベート
・レシーバは Unmarshal 系を除きすべて Time 型(値型)
・オブジェクトを返すメソッドの戻り値もすべて Time 型(値型)
引用: https://skatsuta.github.io/2015/12/29/value-receiver-pointer-receiver/
一度定義されれば不変の値となるので、値を変更するためにポインターにする必要がないとのことです。
レシーバーのタイプが明確に判別できない場合は、ポインター型が推奨されているようです。値が書き変わるリスクはありそうな気がしますが、値レシーバー利用によるコピーのコストを考慮しているのかと思いました。
以上、関数のレシーバータイプの決め方でした!
Receivers and Interfaces
Pointers vs. Values
Receiver Type
pospomeのプログラミング日記
Go言語のスタックとヒープ
診断を受けるとあなたの現在の業務委託単価を算出します。今後副業やフリーランスで単価を交渉する際の参考になります。また次の単価レンジに到達するためのヒントも確認できます。