web-dev-qa-db-ja.com

バリューレシーバーとポインターレシーバー

その場合、常にポインターレシーバーを使用するのではなく、バリューレシーバーを使用することは非常に不明確です。
ドキュメントの要約:

type T struct {
    a int
}
func (tv  T) Mv(a int) int         { return 0 }  // value receiver
func (tp *T) Mp(f float32) float32 { return 1 }  // pointer receiver

docsは、「基本型、スライス、小さな構造体などの型の場合、値レシーバーは非常に安価であるため、メソッドのセマンティクスがポインター、値レシーバーを必要としない限り、効率的で明確です。」

最初のポイントそれは「非常に安い」と言っていますが、問題はポインター受信機よりも安いということです。そこで、小さなベンチマーク (Gistのコード) を作成しました。これは、ポインターレシーバーが文字列フィールドを1つだけ持つ構造体であっても高速であることを示しています。結果は次のとおりです。

// Struct one empty string property
BenchmarkChangePointerReceiver  2000000000               0.36 ns/op
BenchmarkChangeItValueReceiver  500000000                3.62 ns/op


// Struct one zero int property
BenchmarkChangePointerReceiver  2000000000               0.36 ns/op
BenchmarkChangeItValueReceiver  2000000000               0.36 ns/op

(編集:新しいバージョンでは2番目のポイントが無効になったことに注意してください。コメントを参照)
2番目のポイント、それは「効率的で明確」だと言いますが、これはもっと好みの問題ですね。個人的には、どこでも同じように使用することで一貫性を好みます。どんな意味で効率ですか?パフォーマンスに関しては、ほとんどの場合、ポインターの方が効率的です。 1つのintプロパティを使用したテスト実行では、Valueレシーバーの最小の利点が示されました(範囲は0.01〜0.1 ns/op)

値受信者がポインター受信者より明らかに理にかなっているケースを誰かに教えてもらえますか?または、ベンチマークで何か間違ったことをしていますか、他の要因を見落としていましたか?

83
Chrisport

FAQは一貫性について言及しています

次は一貫性です。型のメソッドの一部にポインターレシーバーが必要な場合、残りにもポインターレシーバーが必要であるため、メソッドセットは、型の使用方法に関係なく一貫しています。詳細については、 メソッドセットのセクション sを参照してください。

前述のように このスレッドで

レシーバーのポインターと値に関するルールは、ポインターと値に対して値メソッドを呼び出すことができますが、ポインターメソッドはポインターに対してのみ呼び出すことができるということです

今:

値受信者がポインター受信者より明らかに理にかなっているケースを誰かに教えてもらえますか?

コードレビューコメント が役立ちます。

  • 受信者がマップ、func、またはchanの場合、それへのポインターを使用しないでください。
  • レシーバーがスライスであり、メソッドがスライスの再スライスまたは再割り当てを行わない場合、そのポインターを使用しないでください。
  • メソッドがレシーバーを変更する必要がある場合、レシーバーはポインターでなければなりません。
  • 受信者が_sync.Mutex_または類似の同期フィールドを含む構造体である場合、受信者はコピーを避けるためにポインターでなければなりません。
  • レシーバーが大きな構造体または配列の場合、ポインターレシーバーの方が効率的です。どれくらい大きいですか?すべての要素を引数としてメソッドに渡すことと同等であると仮定します。それが大きすぎると感じた場合、受信機にとっても大きすぎます。
  • 関数またはメソッドは、同時に、またはこのメソッドから呼び出されたときに、レシーバーを変更できますか?値型は、メソッドが呼び出されるときにレシーバーのコピーを作成するため、外部の更新はこのレシーバーに適用されません。元のレシーバーで変更を表示する必要がある場合、レシーバーはポインターでなければなりません。
  • レシーバーが構造体、配列、またはスライスであり、その要素のいずれかが変化している可能性のあるものへのポインターである場合は、ポインターレシーバーを優先します。
  • レシーバーが自然に値型である小さな配列または構造体の場合(たとえば、_time.Time_型のような)、可変フィールドおよびポインターなし、または単なる単純型intやstringなどの基本型、値の受信者が意味をなす
    値の受信者は、生成可能なガベージの量を減らすことができます。値がvalueメソッドに渡される場合、ヒープに割り当てる代わりに、スタック上のコピーを使用できます。(コンパイラは、この割り当てを回避することを賢くしようとしますが、常に成功するとは限りません。)最初にプロファイリングせずに、この理由で値の受信者タイプを選択しないでください。
  • 最後に、疑わしい場合は、ポインターレシーバーを使用します。

太字の部分は、たとえば net/http/server.go#Write() にあります。

_// Write writes the headers described in h to w.
//
// This method has a value receiver, despite the somewhat large size
// of h, because it prevents an allocation. The escape analysis isn't
// smart enough to realize this function doesn't mutate h.
func (h extraHeader) Write(w *bufio.Writer) {
...
}
_
101
VonC

@VonCにさらに有益な回答を追加します。

プロジェクトが大きくなり、古い開発者が去り、新しい開発者が登場すると、メンテナンスコストについて誰も本当に言及しなかったことに驚いています。 Goは確かに若い言語です。

一般的に言えば、私はできる限りポインターを避けようとしますが、それらには場所と美しさがあります。

次の場合にポインタを使用します:

  • 大規模なデータセットでの作業
  • 状態を維持する構造体があります。 TokenCache、
    • すべてのフィールドがプライベートであることを確認します。相互作用は、定義されたメソッドレシーバーを介してのみ可能です。
    • この関数をゴルーチンに渡さない

例えば:

type TokenCache struct {
    cache map[string]map[string]bool
}

func (c *TokenCache) Add(contract string, token string, authorized bool) {
    tokens := c.cache[contract]
    if tokens == nil {
        tokens = make(map[string]bool)
    }

    tokens[token] = authorized
    c.cache[contract] = tokens
}

ポインタを避ける理由:

  • ポインターは同時に安全ではありません(GoLangの要点)
  • 一度ポインター受信機、常にポインター受信機(一貫性のためのすべてのStructのメソッド)
  • ミューテックスは、「バリューコピーコスト」と比較して、確かに高価で、遅く、保守が困難です。
  • 「価値コピーコスト」と言えば、それは本当に問題なのでしょうか?早すぎる最適化はすべての悪の根源であり、いつでもポインターを追加できます
  • 直接、意識的に小さなStructsを設計するように強制します
  • 明確な意図と明白なI/Oを持つ純粋な関数を設計することで、ポインターをほとんど回避できます。
  • ガベージコレクションは、私が信じているポインタでは難しい
  • カプセル化、責任について議論しやすい
  • シンプルで馬鹿げたままにしてください(はい、次のプロジェクトの開発者を知らないので、ポインターは扱いにくい場合があります)
  • 単体テストはピンク色の庭を歩くようなものです(スロバキア語のみの表現ですか?)。
  • 条件なしのNILなし(ポインタが期待されていた場所にNILを渡すことができます)

私の経験則では、次のようなカプセル化されたメソッドをできるだけ多く記述します。

package rsa

// EncryptPKCS1v15 encrypts the given message with RSA and the padding scheme from PKCS#1 v1.5.
func EncryptPKCS1v15(Rand io.Reader, pub *PublicKey, msg []byte) ([]byte, error) {
    return []byte("secret text"), nil
}

cipherText, err := rsa.EncryptPKCS1v15(Rand, pub, keyBlock) 

UPDATE:

この質問により、私はこのトピックをさらに調査し、それに関するブログ投稿を書くことになりました https://medium.com/gophersland/Gopher-vs-object-oriented-golang-4fa62b88c701

11
BlocksByLukas