1. ホーム
  2. pointers

[解決済み] パラメータと戻り値におけるポインタと値の比較

2022-03-22 05:50:54

質問

を返す方法はいろいろあります。 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つ目は、1つの構造体のインスタンスを呼び出し間で簡単に再利用できるため、非常に高いメモリ効率を実現したい場合です。どのような場合に使用するのか、ベスト プラクティスはありますか?

同様に、スライスに関しても質問です。

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 )?

解決方法は?

tl;dr :

  • 受信機ポインタを使用するメソッドが一般的です。 レシーバーの経験則は 疑問があれば、ポインターを使いましょう。
  • スライス、マップ、チャンネル、文字列、関数値、インターフェース値などは、内部でポインターを用いて実装されており、それらへのポインターは冗長であることが多いのです。
  • その他、大きな構造体や変更が必要な構造体にはポインターを使用し、そうでない場合は 値を渡す ポインタを介して不意に変更されると混乱するからです。

ポインターをよく使うべきケースの一つ。

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

ポインタが必要ない場面もある。

  • コードレビューのガイドラインでは 小さな構造体 のように type Point struct { latitude, longitude float64 } ただし、呼び出す関数がその場で変更できる必要がある場合はこの限りではありません。

    • 値のセマンティクスは、こっちの代入があっちの値を不意に変えてしまうようなエイリアシングの状況を回避します。
    • きれいなセマンティクスを犠牲にして少しでも速くするのはGo-yではありませんし、小さな構造体を値で渡す方が実際に効率的な場合もあります。 キャッシュミス やヒープの確保が必要です。
    • そこで、Go Wikiの コードレビューコメント ページでは、構造体が小さく、その状態が続く可能性が高い場合に、値で渡すことを提案しています。
    • 多くの構造体が、ポインタでも値でも問題ない範囲にあることは間違いありません。下限値として、コード レビュー コメントでは、値の受信機としてスライス(3 つのマシン語)を使用するのが妥当であると示唆されています。上限値としては bytes.Replace は10ワード分の引数(3つのスライスと int ). を見つけることができます。 シチュエーション 大きな構造体でもコピーすることでパフォーマンスが向上することがありますが、経験則から言うとコピーしない方がいいです。
  • について スライス の場合、配列の要素を変更するためにポインタを渡す必要はありません。 io.Reader.Read(p []byte) のバイトを変更します。 p 例えば これは間違いなく、quot; treat little structs like values の特殊なケースです。 スライスヘッダー (参照 ラス・コックス(rsc)氏の説明 ). 同じように、ポインタを マップを変更したり、チャネルで通信する .

  • について スライスする (の開始/長さ/容量を変更する)ような組み込み関数があります。 append はスライス値を受け取り、新しいスライス値を返します。エイリアシングを避けることができ、新しいスライスを返すことで、新しい配列が割り当てられるかもしれないという事実に注意を向けることができ、呼び出し側にも馴染みやすいからです。

    • このパターンが常に実用的とは限りません。以下のようなツールもあります。 データベースインターフェース または シリアライザー は、コンパイル時に型がわからないスライスに追加する必要があります。彼らは時々、スライスへのポインタを interface{} パラメータで指定します。
  • マップ、チャンネル、文字列、関数とインターフェースの値 は、スライスと同様、内部的には参照または参照をすでに含む構造体なので、基礎となるデータがコピーされるのを避けるだけなら、ポインターを渡す必要はありません。(rsc インターフェイスの値がどのように格納されるかについては、別の記事で紹介しています。 ).

    • 稀にポインターを渡す必要がある場合があります。 モディファイ を呼び出すと、呼び出し元の構造体が表示されます。 flag.StringVar を取ります。 *string というような理由で、例えば

ポインターを使用するところ。

  • ポインタが必要な構造体のメソッドとして関数を作成するかどうかを検討します。人々は、多くのメソッドを x を修正するために x そのため、変更された構造体を受信者にすることで、驚きを最小限に抑えることができるかもしれません。また ガイドライン は、レシーバがポインタであるべき場合について説明します。

  • レシーバ以外のパラメータに効果を持つ関数は、そのことを godoc で明確にするか、よりよい方法として godoc と名前(例えば reader.WriteTo(writer) ).

  • メモリの再利用のためにAPIを変更するのは、割り当てに自明でないコストがかかることが明らかになるまで遅らせ、その後、すべてのユーザーにトリッキーなAPIを強要しない方法を探します。

    1. アロケーションを回避するために、Goの エスケープ分析 はあなたの味方です。些細なコンストラクタやリテラルで初期化できる型、あるいは bytes.Buffer .
    2. を考えてみましょう。 Reset() メソッドでオブジェクトをブランクの状態に戻すことができます。気にしないユーザーや、割り当てを保存できないユーザーは、これを呼び出す必要はありません。
    3. 便宜上、modify-in-placeメソッドとcreate-from-scratch関数をマッチングペアで記述することを検討してください。 existingUser.LoadFromJSON(json []byte) error でラップすることができます。 NewUserFromJSON(json []byte) (*User, error) . ここでもまた、遅延と割り当てのピンチの間の選択を個々の呼び出し元に押し付けることになります。
    4. メモリを再利用しようとする呼び出し側は sync.Pool は、いくつかの詳細を処理します。特定のアロケーションが多くのメモリを圧迫する場合、そのアロケーションがいつ使われなくなるかを知っていて、より良い最適化が利用できないか確信しているのでしょう。 sync.Pool が役に立ちます。(CloudFlareが公開 は、便利な(事前に sync.Pool ブログ記事 リサイクルについて)

最後に、スライスをポインターにするかどうかですが、値のスライスは便利で、アロケーションとキャッシュミスを減らすことができます。ブロッカーが存在する可能性があります。

  • アイテムを作成するためのAPI を呼び出す必要があるなど、ポインタを強制されることがあります。 NewFoo() *Foo で初期化させるのではなく ゼロ値 .
  • 希望するアイテムのライフタイム はすべて同じとは限りません。99% のアイテムはもう使えないけれども、残りの 1% へのポインタがある場合、配列はすべて確保されたままです。
  • 値の移動 は、パフォーマンスや正しさの問題を引き起こす可能性があるため、ポインタはより魅力的なものとなっています。注目すべきは append は、アイテムをコピーするときに 配列が成長する . の前に取得したポインタは append が間違った場所を指すと、巨大な構造体ではコピーに時間がかかり、また、例えば sync.Mutex をコピーすることは許されない。途中の挿入/削除やソートも同様に項目を移動させます。

大まかに言えば、バリュースライスが意味を持つのは、すべてのアイテムを前もって配置し、それらを動かさない場合(たとえば、これ以上 append あるいは、アイテムへのポインターを使用しない、アイテムは効率的にコピーできるほど小さい、など)。具体的に考えたり測ったりする必要がある場合もありますが、大まかな目安にはなると思います。