パラメータと戻り値のポインタと値


329

Goでは、struct値またはそのスライスを返すさまざまな方法があります。私が見た個々のものについて:

type MyStruct struct {
    Val int
}

func myfunc() MyStruct {
    return MyStruct{Val: 1}
}

func myfunc() *MyStruct {
    return &MyStruct{}
}

func myfunc(s *MyStruct) {
    s.Val = 1
}

これらの違いを理解しています。1つ目は構造体のコピーを返し、2つ目は関数内で作成された構造体値へのポインタ、3つ目は既存の構造体が渡されることを期待して値をオーバーライドします。

これらのパターンのすべてがさまざまなコンテキストで使用されるのを見てきましたが、これらに関するベストプラクティスは何なのでしょうか。どちらを使用しますか?たとえば、最初のものは小さな構造体で問題がなく(オーバーヘッドが最小限であるため)、2つ目はより大きな構造体で問題ありません。また、呼び出し間で単一の構造体インスタンスを簡単に再利用できるため、非常にメモリ効率を高めたい場合は3番目。どれを使用するかについてのベストプラクティスはありますか?

同様に、スライスに関する同じ質問:

func myfunc() []MyStruct {
    return []MyStruct{ MyStruct{Val: 1} }
}

func myfunc() []*MyStruct {
    return []MyStruct{ &MyStruct{Val: 1} }
}

func myfunc(s *[]MyStruct) {
    *s = []MyStruct{ MyStruct{Val: 1} }
}

func myfunc(s *[]*MyStruct) {
    *s = []MyStruct{ &MyStruct{Val: 1} }
}

ここでも、ベストプラクティスは何ですか。スライスは常にポインタであることを知っているので、スライスへのポインタを返すことは役に立ちません。しかし、構造体の値のスライス、構造体へのポインターのスライスを返す必要がありますか?スライスへのポインターを引数として渡す必要がありますか(Go App Engine APIで使用されるパターン)?


1
あなたが言うように、それは本当にユースケースに依存します。状況に応じてすべて有効です-これは可変オブジェクトですか?コピーまたはポインタが必要ですか?等ところで、あなたはnew(MyStruct):)の使用について言及しませんでしたが、実際には、ポインタを割り当てて返す方法の違いはありません。
Not_a_Golfer

15
それは文字通りエンジニアリングを超えています。構造体はかなり大きくなければならず、ポインタを返すとプログラムが高速になります。気にしないでください。コード、プロファイル、必要に応じて修正してください。
Volker

1
値またはポインタを返す方法は1つだけです。それは、値またはポインタを返すことです。それらをどのように割り当てるかは、別の問題です。自分の状況に合った方法を使用し、心配する前にコードを書いてください。
JimB 2014年

3
ところで、私は好奇心から、これをベンチマークしました。構造体とポインタを返す速度はほぼ同じように見えますが、関数にポインタを渡すと、速度が大幅に向上します。レベルではありませんが重要です
Not_a_Golfer

1
@Not_a_Golfer:bc割り当てが関数の外で行われているだけだと思います。また、値とポインタのベンチマークは、事後の構造体とメモリアクセスパターンのサイズに依存します。キャッシュラインサイズのもののコピーは可能な限り高速であり、CPUキャッシュからのポインターの逆参照の速度は、メインメモリからの逆参照とは大きく異なります。
JimB 2014年

回答:


392

tl; dr

  • レシーバーポインターを使用する方法は一般的です。レシーバーの経験則は、「疑わしい場合はポインターを使用する」です。
  • スライス、マップ、チャネル、文字列、関数値、およびインターフェース値は内部的にポインターで実装され、それらへのポインターは冗長であることがよくあります。
  • それ以外の場合は、大きな構造体または変更する必要のある構造体にポインターを使用します。そうでない場合は、値を渡す必要があります。これは、ポインターを使用して予期せずに変更を加えるのは難しいためです。

ポインターを頻繁に使用する必要がある1つのケース:

  • レシーバー は、他の引数よりも頻繁にポインターです。メソッドが呼び出されるものを変更したり、名前付きの型を大きな構造体にしたりすることは珍しいことではないのでまれな場合を除いて、ガイダンスはデフォルトでポインターに設定することです。
    • Jeff Hodgesのcopyfighterツールは、値で渡される非小型のレシーバーを自動的に検索します。

ポインタが不要な状況:

  • コードレビューガイドラインでは、呼び出す関数がそれらを適切に変更できるようにする必要がある場合を除き、のような小さな構造体を渡すことを推奨しtype Point struct { latitude, longitude float64 }ています。

    • 値のセマンティクスにより、ここでの割り当てによってあっという間に値が変化するエイリアシングの状況が回避されます。
    • クリーンなセマンティクスを少しの速度で犠牲にすることはGo-yではありません。キャッシュミスやヒープの割り当てを回避するため、小さな構造体を値で渡す方が実際には効率的です。
    • そのため、Go Wikiのコードレビューのコメントページでは、構造体が小さく、そのままである可​​能性が高い場合に値渡しを提案しています。
    • 「大きな」カットオフが曖昧に思える場合、それはそうです。おそらく、多くの構造体は、ポインターまたは値のどちらでも問題ない範囲にあります。下限として、コードレビューコメントは、スライス(3つのマシンワード)が値のレシーバーとして使用するのが妥当であることを示唆しています。上限に近いものとして、bytes.Replace10ワード分の引数(3つのスライスとint)を取ります。
  • スライス、あなたは、配列の変化素子へのポインタを渡す必要はありません。たとえばのio.Reader.Read(p []byte)バイトを変更pします。内部的にはスライスヘッダーと呼ばれる小さな構造体を渡すため、これは「値のような小さな構造体を扱う」ことの間違いなく特殊なケースです(Russ Cox(rsc)の説明を参照)。同様に、マップ変更したり、チャネルで通信したりするためのポインターは必要ありません。

  • スライスするスライス(開始/長さ/容量を変更する)の場合append、スライス値を受け入れて新しい値を返すなどの組み込み関数。私はそれを真似します。エイリアシングを回避し、新しいスライスを返すことで、新しい配列が割り当てられる可能性があるという事実に注意を向けるのに役立ち、呼び出し元にはよく知られています。

    • そのパターンに従うのは必ずしも実用的ではありません。データベースインターフェイスシリアライザなどの一部のツールでは、コンパイル時に型が不明なスライスに追加する必要があります。それらは、interface{}パラメーター内のスライスへのポインターを受け入れる場合があります。
  • マップ、チャネル、文字列、およびスライスのような関数とインターフェイスの値は、内部参照または既に参照を含む構造体であるため、基礎となるデータのコピーを回避するだけの場合は、それらにポインターを渡す必要はありません。 。(rsc は、インターフェイスの値がどのように格納されるかについて別の投稿を書きました)。

    • まれに、呼び出し元の構造体を変更する必要がある場合でも、ポインタを渡す必要があります。たとえば、そのためにflag.StringVara *stringを受け取ります。

ポインタを使用する場所:

  • 関数が、ポインタが必要な構造体のメソッドであるかどうかを検討してください。人々は多くのメソッドxを変更することを期待しているxので、変更された構造体をレシーバーにすることで驚きを最小限に抑えることができます。レシーバーをポインターにする必要がある場合のガイドラインがあります。

  • 非受信者パラメーターに影響を与える関数は、それをgodocで明確にする必要があります。さらに良いことに、godocと名前(などreader.WriteTo(writer)

  • 再利用を許可することで割り当てを回避するためにポインターを受け入れることについて言及しています。メモリの再利用のためにAPIを変更することは最適化であり、割り当てに重要なコストがかかることが明らかになるまで遅らせ、その後、すべてのユーザーにトリッキーなAPIを強制しない方法を探します。

    1. 割り当てを回避するために、Goのエスケープ分析はあなたの友人です。単純なコンストラクター、プレーンリテラル、またはのような便利なゼロ値で初期化できる型を作成することにより、ヒープ割り当てを回避できる場合がありますbytes.Buffer
    2. Reset()いくつかのstdlib型が提供するように、オブジェクトを空白の状態に戻す方法を検討してください。気にしない、または割り当てを保存できないユーザーは、それを呼び出す必要はありません。
    3. インプレース変更メソッドとスクラッチ作成関数を一致するペアとして作成することを検討してください。便宜上、でexistingUser.LoadFromJSON(json []byte) errorラップできますNewUserFromJSON(json []byte) (*User, error)。繰り返しになりますが、それは、遅延とピンチ割り当ての間の選択を個々の呼び出し元にプッシュします。
    4. メモリをリサイクルしようとする呼び出し元は、sync.Poolいくつかの詳細を処理できます。特定の割り当てが多くのメモリのプレッシャーを生み出す場合、割り当てが使用されなくなったことを確信していて、利用sync.Poolできる最適化がない場合に役立ちます。(CloudFlareはsync.Poolリサイクルに関する有用な(事前)ブログ投稿を公開しまし。)

最後に、スライスをポインタにする必要があるかどうかについて説明します。値のスライスは便利で、割り当てとキャッシュミスを節約できます。ブロッカーが存在する可能性があります:

  • アイテムを作成するためのAPIは、ポインタを強制する場合があります。たとえばNewFoo() *Foo、Goをゼロ値で初期化するのではなく、呼び出す必要があります
  • アイテムの望ましい寿命はすべて同じではない場合があります。スライス全体が一度に解放されます。アイテムの99%がもはや役に立たないが、他の1%へのポインターがある場合、すべての配列が割り当てられたままになります。
  • アイテムを移動すると、問題が発生する可能性があります。特に、基になる配列が大きくなるとappendアイテムをコピーします。先に取得したポインターが後の間違った場所を指している場合、巨大な構造体ではコピーが遅くなる可能性があります。たとえば、コピーは許可されません。中央に挿入/削除し、同様に並べ替えを行うとアイテムが移動します。appendsync.Mutex

大まかに言って、値のスライスは、すべてのアイテムを前に配置して移動しない場合(たとえば、append初期設定後はsがなくなるなど)、または移動し続けても確実に移動する場合に有効です。 OK(アイテムへのポインタを使用しない/注意深く使用する、アイテムが効率的にコピーできるほど小さいなど)。状況の詳細を考えたり測定したりする必要がある場合もありますが、それは大まかなガイドです。


12
大きな構造体とはどういう意味ですか?大きな構造体と小さな構造体の例はありますか?
帽子のないユーザー

1
バイトをどのように確認しますか?Replaceはamd64で80バイトの引数を取りますか?
Tim Wu

2
署名はReplace(s, old, new []byte, n int) []byte; s、old、newはそれぞれ3ワード(スライスヘッダーは(ptr, len, cap))で、n int1ワードなので、10ワードです。つまり、8バイト/ワードでは80バイトです。
twotwotwo 2015年

6
大きな構造体をどのように定義しますか?どのくらいの大きさですか?
アンディアルド

3
@AndyAldo私のソース(コードレビューコメントなど)のいずれもしきい値を定義していないため、しきい値を設定するのではなく、判断の呼び出しであると判断しました。3つの単語(スライスなど)は、一貫してstdlibの値として適格として扱われます。5ワードの値のレシーバーのインスタンスを今見つけました(text / scanner.Position)が、それについてはあまり読みません(ポインターとしても渡されます!)。ベンチマークなどがなければ、読みやすくするのに最も便利だと思われることは何でもします。
twotwotwo 2017年

10

メソッドレシーバーをポインターとして使用する3つの主な理由:

  1. 「最初に、そして最も重要なこととして、メソッドはレシーバーを変更する必要がありますか?変更する場合、レシーバーはポインターでなければなりません。」

  2. 「2つ目は効率の考慮です。レシーバーが大きい場合、たとえば大きな構造体の場合、ポインターレシーバーを使用する方がはるかに安価です。」

  3. 「次は一貫性です。型の一部のメソッドにポインターレシーバーが必要な場合、残りも必要です。そのため、型の使用方法に関係なく、メソッドセットは一貫しています。」

リファレンス:https : //golang.org/doc/faq#methods_on_values_or_pointers

編集:もう1つの重要なことは、機能に送信する実際の「タイプ」を知ることです。タイプは「値タイプ」または「参照タイプ」のいずれかです。

スライスとマップは参照として機能しますが、関数でスライスの長さを変更するようなシナリオでは、それらをポインターとして渡したい場合があります。


1
2の場合、カットオフは何ですか?私の構造体が大きいか小さいかをどうやって知るのですか?また、(ヒープから参照する必要がないように)ポインタではなく値を使用する方が効率的であるほど小さい構造体はありますか?
zlotnika 2018年

フィールドやネストされた構造体の数が多いほど、構造体は大きくなります。構造体がいつ「ビッグ」または「ラージ」と呼ばれるかを知るための特定のカットオフまたは標準的な方法があるかどうかはわかりません。構造体を使用または作成している場合、上記で述べたことに基づいて、構造体が大きいか小さいかがわかります。しかし、それは私だけです。
Santosh Pillai

2

一般にポインタを返す必要があるのは、ステートフルまたは共有可能なリソースのインスタンスを作成するときです。これは、多くの場合、接頭辞が付いた関数によって行われますNew

それらは何かの特定のインスタンスを表し、いくつかのアクティビティを調整する必要がある場合があるため、同じリソースを表す複製/コピーされた構造を生成することはあまり意味がありません-返されたポインタはリソース自体のハンドルとして機能します。

いくつかの例:

その他の場合、構造体が大きすぎてデフォルトでコピーできない可能性があるという理由だけでポインタが返されます。


あるいは、代わりに内部的にポインターを含む構造のコピーを返すことにより、直接ポインターを返すことを回避できますが、これは慣用的とは見なされません。


この分析で暗黙的に示されいるのは、デフォルトでは、構造体は値によってコピーされるということです(ただし、必ずしもそれらの間接的なメンバーではありません)。
nobar

2

可能であれば(たとえば、参照として渡す必要のない非共有リソース)、値を使用します。次の理由により:

  1. コードは、ポインタ演算子とnullチェックを回避し、より見やすく、読みやすくなります。
  2. コードは、ヌルポインターパニックに対してより安全になります。
  3. あなたのコードはしばしばより速くなります:はい、より速く!どうして?

理由1:スタックに割り当てるアイテムが少なくなります。スタックからの割り当て/割り当て解除は即時に行われますが、ヒープでの割り当て/割り当て解除は非常にコストがかかる場合があります(割り当て時間+ガベージコレクション)。ここでいくつかの基本的な数字を見ることができます:http : //www.macias.info/entry/201802102230_go_values_vs_references.md

理由2:特に戻り値をスライスに格納する場合、メモリオブジェクトはメモリ内でよりコンパクトになります。すべてのアイテムが連続しているスライスをループすることは、すべてのアイテムがメモリの他の部分へのポインターであるスライスを繰り返すよりもはるかに高速です。 。間接的なステップではなく、キャッシュミスの増加のためです。

神話ブレーカー:典型的なx86キャッシュラインは64バイトです。ほとんどの構造体はそれよりも小さいです。メモリ内のキャッシュラインをコピーする時間は、ポインタをコピーする時間と同じです。

コードの重要な部分が遅い場合のみ、私はいくつかのマイクロ最適化を試み、ポインターを使用すると速度がいくらか向上するかどうかを確認しますが、可読性と保守性は低下します。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.