値レシーバーとポインターレシーバー


107

常にポインターレシーバーを使用するのではなく、値レシーバーを使用したい場合は、私には非常に不明確です。
ドキュメントから要約するには:

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

ドキュメントは、「そのような基本的なタイプ、スライス、および小さな構造体などの種類については、値の受信機が非常に安くなっメソッドのセマンティクスは、ポインタを必要としない限り、値の受信機は、効率的かつ明確である。」また言います

最初のポイントは、それが「非常に安い」と述べていますが、問題は、ポインターレシーバーよりも安いということです。そこで、小さなベンチマーク(コードの要旨)作成しました。これは、文字列フィールドが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

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

誰かが値レシーバーがポインターレシーバーよりも明らかに理にかなっている場合を教えてもらえますか?それとも、ベンチマークで何か間違ったことをしていますか?他の要素を見落としましたか?


3
単一の文字列フィールドと2つのフィールド(文字列フィールドとintフィールド)で同様のベンチマークを実行しました。バリューレシーバーからの結果が速くなりました。BenchmarkChangePointerReceiver-4 10000000000 0.99 ns / op BenchmarkChangeItValueReceiver-4 10000000000 0.33 ns / opこれはGo 1.8を使用しています。最後にベンチマークを実行してからコンパイラーの最適化が行われたのかと思います。詳細は要旨をご覧ください。
pbitty 2017年

2
あなたが正しい。Go1.9を使用して元のベンチマークを実行すると、今でも異なる結果が得られます。ポインターレシーバー0.60 ns / op、値レシーバー0.38 ns / op
Chrisport 2017年

回答:


117

FAQは一貫性について言及していることに注意してください

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

このスレッドで述べたよう

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

今:

誰かが値レシーバーがポインターレシーバーよりも明らかに理にかなっている場合を教えてもらえますか?

コードレビューコメントは助けることができます。

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

太字の部分は、たとえば次の場所にあります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) {
...
}

16
The rule about pointers vs. values for receivers is that value methods can be invoked on pointers and values, but pointer methods can only be invoked on pointers 事実ではない。値レシーバーとポインターレシーバーの両方のメソッドは、正しく型付けされたポインターまたは非ポインターで呼び出すことができます。メソッドが呼び出される場所に関係なく、メソッド本体内で、レシーバーの識別子は、値レシーバーが使用されている場合はコピーごとの値を
ハートSimha

3
偉大な説明があり、ここでは 、「xがアドレス指定可能であると&XのメソッドセットはM、XM()がの省略形(&x)が含まれている場合.M()。」
テラ

@teraはい:それはstackoverflow.com/q/43953187/6309
VonC

4
すばらしい答えですが、「意図がより明確になるため」、NOPE、クリーンなAPI、引数としてのX、戻り値としてのYは明確な意図です。Structをポインターで渡し、コードを注意深く読んですべての属性が変更される内容を確認することに時間を費やすことは、明確で保守性に優れているとは言えません。
Lukas Lukac

@HartSimha上記の投稿は、ポインターのレシーバーメソッドが値の型の「メソッドセット」に含まれていないことを示していると思います。リンクされたプレイグラウンドで次の行を追加すると、コンパイルエラーが発生しますInt(5).increment_by_one_ptr()。同様に、メソッドを定義する特性increment_by_one_ptrはtypeの値では満足されませんInt
Gaurav Agarwal

16

@VonCに追加して、すばらしい有益な回答を追加します。

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

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

私は次の場合にポインタを使用します。

  • 大規模なデータセットの操作
  • 構造体を維持する構造体、例えば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のメソッド)
  • ミューテックスは確かに「値のコピーのコスト」と比較して、より高価で、遅く、維持が困難です
  • 「バリューコピーコスト」と言えば、それは本当に問題なのでしょうか?時期尚早な最適化はすべての悪の根源であり、いつでも後でポインタを追加できます
  • 直接、意識的に小さな構造体を設計するように強制します
  • 明確な意図と明白な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) 

更新:

この質問に触発されて、このトピックをさらに調査し、それについてブログに投稿しました。https://medium.com/gophersland/gopher-vs-object-oriented-golang-4fa62b88c701


私はあなたがここで言うことの99%が好きで、強く同意します。それはあなたの例があなたのポイントを説明するための最良の方法であるかどうか私は思っていると言った。TokenCacheは本質的にマップではありません(@VonCから-「レシーバーがマップ、funcまたはchanの場合は、それへのポインターを使用しないでください」)。マップは参照型なので、 "Add()"をポインターレシーバーにすることで何が得られますか?TokenCacheのコピーはすべて同じマップを参照します。このGoプレイグラウンドを参照-play.golang.com/p/Xda1rsGwvhq
リッチ

揃って嬉しいです。素晴らしい点。実際、この例ではTokenCacheがそのマップだけではなく、より多くのものを処理しているプロジェクトからコピーしたため、この例ではポインターを使用したと思います。また、ポインターを1つのメソッドで使用する場合は、すべてのメソッドで使用します。この特定のSOの例からポインタを削除することをお勧めしますか?
Lukas Lukac

笑、コピー/貼り付けストライキが再び!😉IMOは、陥りやすいトラップを示しているためそのままにしておくことも、状態や大規模なデータ構造を示すものにマップを置き換えることもできます。
リッチ

まあ、私は彼らがコメントを読むと確信しています... PS:リッチ、あなたの議論は妥当なようです、LinkedIn(私のプロフィールのリンク)に私を追加して、接続してください。
Lukas Lukac
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.