golang の 引数、戻り値、レシーバをポインタにすべきか、値にすべきかの判断基準について迷っている

日頃から

引数にポインタを渡した方がいいのか? 値を渡した方がいいのか?
戻り値はどーなの?
メソッドのレシーバは?

なんて迷っているのでアウトプットしてみる。

メソッドのレシーバについては以下に載っていた。
https://github.com/golang/go/wiki/CodeReviewComments#receiver-type

以下は日本語訳。
http://qiita.com/knsh14/items/8b73b31822c109d4c497#receiver-type

レシーバについては、これが基準な気がする。

そして、このドキュメントにはレシーバというよりも、
ポインタと値の特徴が載っているので、
それらの特徴を「引数」「戻り値」でも考えていけば、それっぽい答えになりそう。

ということで、「引数」「戻り値」を対象にザックリとまとめてみた。

1. ポインタじゃないと期待通りの動作をしない場合はポインタにする

これはポインタにせざるを得ないパターン。
例えば、引数を書き換えて、それをクライアントコードに反映させたい場合はポインタである必要がある。

以下は Change() は引数の i を 100 に書き換えており、
i = 100 は呼び出し元の main の i にも反映させる。

func main() {
    i := 0
    Change(&i)
    fmt.Println(i) // 100
}

func Change(i *int) {
    *i = 100
}

以下のように値だと i = 100 は呼び出し元の main の i に反映されない。

func main() {
    i := 0
    Change(i)
    fmt.Println(i) // 0
}

func Change(i int) {
    i = 100
}

このように引数に関わらず戻り値でも、
「ポインタじゃないと期待通りの動作をしない場合」は
当然ながらポインタにする必要がある。


これ以降はポインタでも値でもどっちでも期待通りの動作をする場合の判断基準になる。



2. 値だけどポインタっぽいやつは値にする

slice, map, chan, func などは、
それ自体がポインタのようなものなので、
わざわざポインタにする必要はなく、
値で渡してしまっていい。


3. プリミティブな値は値にする

int, string とかは値で渡してしまっていい。
これは値のコピーコストが気にならないからだと思う。

time.Time のように基本的に値として扱う struct も値として扱う。


4. 大きい struct, array の場合はポインタにする

大きい struct, array は値をコピーするコストが大きくなるから、
ポインタにした方がいいよってことなんだと思う。

で、最初に紹介したレシーバのドキュメントによると、
「大きいかどうか」の判断基準は以下らしい。

もし構造体の全ての値を引数に渡すと仮定してください。多すぎると感じたなら、それはポインタにしても良いくらいの大きさです。

自分が新卒の頃に先輩に読まされた CodeComplete には
引数の数は最大でも7個って書いてあったような気がする。

個人的には 4 個くらいから多い印象を受けるので、
struct がそれ以上のフィールドを持つ場合はポインタで渡した方がいいのかなと思っている。

slice はそれ自体がポインタみたいなものなので、
わざわざポインタにする必要はない。

array の場合はどの程度の長さが目安になるんだろう???



5. GCコストを考慮する???

レシーバが値であるほうが良い場合があります。値のみのレシーバは生成されるゴミの量を減らすことができます。もし値がメソッドに渡されると、ヒープ領域にメモリを確保する代わりに、スタックメモリのコピーが走ります。

ヒープとスタックについては以下を読めば分かると思う。
http://najeira.blogspot.jp/2013/10/go.html

ただ、実際どの程度パフォーマンスが問題になるかっていうのは、
それぞれの実装でベンチ取らないと判断しようがないし、
4番のコピーコストとの兼ね合いになるのかなと思う。

ココらへんは正直全然わからない。
(´・ω・`)


6. 書き換えた値を反映させない場合は値にする

以下のように引数の User を書き換えない場合は値にすることで、
「引数の User を書き換えないですよ」
という関数の挙動を伝えることができる。

func PrintUser(u User) {
    fmt.Println(u.Name)
}

この観点では戻り値の場合は気にしなくていいと思う。


まとめ

紹介したポイントは以下。

  • 1. ポインタじゃないと期待通りの動作をしない場合はポインタにする
  • 2. 値だけどポインタっぽいやつは値にする
  • 3. プリミティブな値は値にする
  • 4. 大きい struct, array の場合はポインタにする
  • 5. GCコストを考慮する???
  • 6. 書き換えた値を反映させない場合は値にする

1,2,3 番のケースでは ポインタ or 値 の選択を迷わないかなと思った。

自分が迷うのは4, 6番の2つを考慮するケースの時が多いかな。

例えば、「大きい struct」 を「値を書き換えない関数の引数に渡す」時に、
関数の引数はポインタにするのかどうか?

4番を優先してポインタにするのか?
6番を優先して値にするのか?

type BigStruct struct {

}

//引数の BigStruct は ポインタ or 値 ???
func PrintBigStruct(b BigStruct) {
    fmt.Println(b.Value)
}


この場合、自分はポインタにしている。
理由は以下。

  • 標準パッケージで struct を値で渡すコードをあまり見たことない気がするから。(実際そうなのかは分からない)
  • 引数を書き換えるというコードをあまり書くケースがないので、アプリケーションコードの引数がほぼ値になってしまい、塵も積もってコピーコストが大きくなってパフォーマンスに影響しそうだから。(測定したわけじゃないので、実際分からない)
  • 引数をちょこちょこポインタ or 値で指定するのが面倒。(個人差ある)
  • 引数を ポインタ→値 or 値→ポインタ に変更する際にクライアンコードを全て書き換える必要がある。(ただ、そもそも変更するケースってあるのか?)

理由を挙げてみたけど、
結局決定打のないままポインタにしてる感がある。

この記事の最初で以下のように言及してるけど、
レシーバも結局上記のケースでは悩むのかなと思う。

メソッドのレシーバについては以下に載っていた。
https://github.com/golang/go/wiki/CodeReviewComments#receiver-type

以下は日本語訳。
http://qiita.com/knsh14/items/8b73b31822c109d4c497#receiver-type

レシーバについては、これが基準な気がする。

ちなみに、5番のヒープとスタックについては知っていたけど、
特に考慮してませんでした・・・。
これも4番と同じで計測しないと分からないので、
基準としては扱っていない。

ということで、
ここら辺の判断基準にアドバイスがあれば、
この記事のコメント or twitter でメンションください。

ちなみに、interface の ポインタ or 値 については以下が分かりやすいと思います。
http://otiai10.hatenablog.com/entry/2014/05/27/223556