Go言語

【Go言語】値渡しとポインタ渡しどちらにすべきかわからなかったので、公式FAQを読んだ

タイトルの通りですが、どういう判断で値渡しとポインタ渡しを使い分ければ良いかわからなかったので、公式FAQのPointers and Allocationの「When are function parameters passed by value?」と「Should I define methods on values or pointers?」を和訳してみました。

When are function parameters passed by value?

C言語由来の全ての言語と同じように、Go言語も値渡しです。つまり、関数は常に渡されたもののコピーを取得します。
例えば、int型の値を関数に渡すと、int型の値のコピーが作成されます。ポインターを渡すとポインターのコピーが作成されます。ポインターが指すデータのコピーは作成されません。(このことがメソッドレシーバーにどう影響するかは、後のセクションを見てください。)

MapとSliceの値はポインターのように動作します。つまり、これらの値は、もととなるMapやSliceのデータへのポインターを含む表現(記述子)です。MapやSliceの値をコピーしても、それらが指しているデータはコピーされません。インターフェース値をコピーすると、インターフェース値に格納されているもののコピーが作成されます。インターフェース値が構造体を保持している場合は構造体のコピーが作成されます。インターフェイス値がポインターを保持している場合はポインターのコピーが作成されますが、ポインターが指すデータのコピーは作成されません。

Should I define methods on values or pointers?

func (s *MyStruct) pointerMethod() { } // method on pointer
func (s MyStruct)  valueMethod()   { } // method on value

ポインターに不慣れなプログラマーにとっては、上の2つの例の違いはわかりにくいでしょうが、非常に単純な話です。型にメソッドを定義すると、レシーバー(上記の例でいうs)はメソッドの引数のように振る舞います。レシーバーを値にするかポインターにするかと、関数の引数を値渡しにするかポインター渡しにするかは同じ問題です。これらを判断するときには、いくつか考慮する点があります。

1つ目、最も重要なのが、メソッドがレシーバーを変更する必要があるか?です。もし、そうであるならば、レシーバーはポインターでなれけばなりません。(SliceとMapは参照型なので微妙に話が変わってきますが、例えばスライスの長さを変更するのであれば、レシーバーはポインターでなければなりません。)
上記の例だと、pointerMethodがsのフィールドを変更する場合、呼び出し元は変更内容を認識できますが、valueMethodは呼び出し元の引数のコピーを使用して呼び出されるため、変更内容は呼び出し元にはわかりません。

2つ目は効率の考慮です。もしレシーバーが大きい場合、例えば巨大な構造体の場合、ポインターレシーバーを使う方がはるかに効率的です。

3つ目は一貫性です。もし、同じ型のメソッドでポインターレシーバーを持つものがあるならば、他のメソッドもポインターレシーバーを使うべきです。なので、型の使われ方に関係なくメソッドセットは一貫しています。

基本型やスライス、小さな構造体などの型の場合、値レシーバーは非常に低コストであるため、メソッドの処理内容的にポインターを必要でないならば、値レシーバーは効率的でわかりやすいです。

まとめ

メソッドのレシーバーをポインターにするか、引数をポインターにするかの判断基準は、

  • レシーバー、引数の値を変更する必要があるか?
  • レシーバー、引数が大きな値か?(巨大な構造体など)
  • 同じ型の他のメソッド、関数に合わせる

どれくらい大きな値であればポインターを使うべきかの判断が難しそう。

構造体のフィールド数を変えてベンチマークをとっている方がいたのでリンクを貼っておきます。

Go言語(golang)における値渡しとポインタ渡しのパフォーマンス影響について

参考

公式FAQのPointers and Allocation

おすすめ図書

-Go言語

© 2021 フリエン生活 Powered by AFFINGER5