パラメータと戻り値におけるポインタと値 質問する

パラメータと戻り値におけるポインタと値 質問する

structGo では、値またはそのスライスを返す方法がいろいろあります。私が見た個々の方法は次のとおりです。

type MyStruct struct {
    Val int
}

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

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

func myfunc(s *MyStruct) {
    s.Val = 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 アプリエンジン API)?

ベストアンサー1

要約:

  • レシーバー ポインターを使用する方法が一般的です。受信者の経験則は「疑問がある場合は、ポインタを使用してください。」
  • スライス、マップ、チャネル、文字列、関数値、およびインターフェース値は、内部的にポインタを使用して実装されており、それらへのポインタは冗長になることがよくあります。
  • それ以外の場所では、大きな構造体や変更が必要な構造体にはポインタを使用し、それ以外の場合は値を渡すポインターを介して突然変更されると混乱が生じるためです。

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

  • レシーバは他の引数よりもポインタであることが多い。メソッドが呼び出されたものを変更したり、名前付き型が大きな構造体になることは珍しくないので、ガイダンスはまれな場合を除き、デフォルトでポインタを使用します。
    • ジェフ・ホッジスコピーファイターツールは、値によって渡される非小さなレシーバーを自動的に検索します。

ポインタが必要ない状況:

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

    • 値のセマンティクスにより、こちら側の割り当てによってあちら側の値が予期せず変更されるというエイリアシング状況を回避できます。
    • 小さな構造体を値渡しすると、次のようなことを避けることでより効率的になります。キャッシュミスまたはヒープ割り当て。いずれにしても、ポインタと値のパフォーマンスが似ている場合、Go 風のアプローチでは、速度を最大限に引き出すのではなく、より自然なセマンティクスを提供するものを選択します。
    • それで、Go Wikiのコードレビューコメントこのページでは、構造体が小さく、その状態が続く可能性が高い場合に値渡しすることを提案しています。
    • 「大きい」というカットオフが曖昧に思えるなら、それはその通りです。おそらく、多くの構造体はポインタか値のどちらかが許容される範囲にあります。下限として、コードレビューのコメントでは、スライス(3つのマシンワード)が値のレシーバーとして合理的であると示唆しています。上限に近いものとしては、bytes.Replace10ワード分の引数(3つのスライスとint)を取ります。状況大きな構造体をコピーしてもパフォーマンスは向上しますが、原則としてそうではありません。
  • スライスの場合、配列の要素を変更するためにポインタを渡す必要はありません。たとえばio.Reader.Read(p []byte)、 は のバイトを変更します。これは、内部的にスライスヘッダーpと呼ばれる小さな構造体を渡しているため、「小さな構造体を値のように扱う」という特別なケースと言えます(ラス・コックス(RSC)の説明同様に、マップを変更したり、チャネルで通信したりする場合にもポインタは必要ありません。

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

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

    • 呼び出し元の構造体を変更したいというまれなケースでは、ポインターを渡す必要がある場合があります。flag.StringVar*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() *FooGoで初期化するのではなく、ゼロ値
  • アイテムの望ましい存続期間は、すべて同じではない可能性があります。スライス全体が一度に解放されます。アイテムの 99% が不要になったが、残りの 1% へのポインターがある場合は、配列全体が割り当てられたままになります。
  • 値をコピーまたは移動すると、appendパフォーマンスや正確性に問題が生じる可能性があるため、ポインターがより魅力的になります。特に、基礎となる配列を拡張する. 前のスライス項目へのポインタは、append項目がコピーされた場所を指していない可能性があり、巨大な構造体ではコピーが遅くなることがあり、たとえば、sync.Mutexコピーは許可されていません。途中での挿入/削除や並べ替えでも項目が移動するため、同様の考慮事項が適用されます。

一般的に、バリュー スライスは、すべてのアイテムを事前に配置して移動しない場合 (たとえば、append初期セットアップ後に s がなくなる)、またはアイテムを移動し続けるが、それが問題ないと確信している場合 (アイテムへのポインターを使用しないか慎重に使用し、アイテムが小さいか、パフォーマンスへの影響を測定した場合) に意味を持ちます。場合によっては、状況に固有のものになることもありますが、これは大まかなガイドです。

おすすめ記事