값 수신기 대 포인터 수신기


108

항상 포인터 수신기를 사용하는 대신 값 수신기를 사용하고 싶은 경우에는 매우 불분명합니다.
문서에서 요약하려면 :

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

문서는 "같은 기본 유형, 조각, 작은 구조체와 같은 유형의 경우, 값 수신기가 아주 싼 방법의 의미가 포인터를 요구하지 않는, 그래서 값 수신기가 효율적이고 분명하다."또한 말한다

첫 번째 요점 은 "매우 저렴"하다고 말하지만 포인터 수신기보다 더 저렴하다는 것입니다. 그래서 저는 작은 벤치 마크 (요점에 대한 코드)를 만들었습니다. 그 포인터 수신기는 문자열 필드가 하나 뿐인 구조체에서도 더 빠릅니다. 결과는 다음과 같습니다.

// 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 버전에서는 두 번째 포인트가 유효하지 않게되었습니다 . 주석 참조) .
두 번째 요점 은 "효율적이고 명확하다"는 것이 맛의 문제가 아닙니다. 개인적으로 나는 모든 곳에서 동일한 방식으로 일관성을 선호합니다. 어떤 의미에서 효율성? 성능면에서 포인터가 거의 항상 더 효율적으로 보입니다. 하나의 int 속성을 사용하는 몇 번의 테스트 실행은 Value 수신기의 이점을 최소화했습니다 (범위 0.01-0.1 ns / op).

누군가 값 수신기가 포인터 수신기보다 명확하게 이해되는 경우를 말할 수 있습니까? 아니면 벤치 마크에서 뭔가 잘못하고 있습니까? 다른 요인을 간과 했습니까?


3
단일 문자열 필드와 두 필드 (문자열 및 정수 필드)로 유사한 벤치 마크를 실행했습니다. 가치 수신기에서 더 빠른 결과를 얻었습니다. BenchmarkChangePointerReceiver-4 10000000000 0.99 ns / op BenchmarkChangeItValueReceiver-4 10000000000 0.33 ns / op Go 1.8을 사용하고 있습니다. 마지막으로 벤치 마크를 실행 한 이후로 컴파일러 최적화가 이루어 졌는지 궁금합니다. 자세한 내용은 요점 을 참조하십시오.
pbitty

2
네가 옳아. Go1.9를 사용하여 원래 벤치 마크를 실행하면 지금도 다른 결과를 얻습니다. 포인터 수신기 0.60 ns / op, 값 수신기 0.38 ns / op
Chrisport 2017

답변:


118

참고 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 사실은 아닙니다. 값 수신기 및 포인터 수신기 메서드는 모두 올바른 형식의 포인터 또는 비 포인터에서 호출 할 수 있습니다. 메서드가 호출되는 내용에 관계없이 메서드 본문 내에서 수신자의 식별자는 값 수신자가 사용되는 경우 복사 별 값을 참조하고 포인터 수신자가 사용되는 경우 포인터를 참조합니다. play.golang.org/p
Hart Simha

3
여기에 훌륭한 설명이 있습니다. "x가 주소 지정이 가능하고 & x의 메소드 세트에 m이 포함되어 있으면 xm ()은 (& x) .m ()의 속기입니다."
tera

예 @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유형 값으로 만족되지 않습니다 Int.
Gaurav Agarwal

16

@VonC에 추가로 유익하고 유익한 답변을 추가하십시오.

프로젝트가 커지고 오래된 개발자가 떠나고 새로운 개발자가 나오면 아무도 유지 관리 비용을 실제로 언급하지 않은 것에 놀랐습니다. Go는 확실히 젊은 언어입니다.

일반적으로 나는 할 수있을 때 포인터를 피하려고하지만 그 자리와 아름다움을 가지고 있습니다.

다음과 같은 경우 포인터를 사용합니다.

  • 대규모 데이터 세트 작업
  • 상태를 유지하는 구조체 (예 : TokenCache)
    • 모든 필드가 PRIVATE인지 확인하고 상호 작용은 정의 된 메서드 수신기를 통해서만 가능합니다.
    • 이 함수를 고 루틴에 전달하지 않습니다.

예 :

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) 

최신 정보:

이 질문은 저에게 주제를 더 많이 연구하고 이에 대한 블로그 게시물을 작성하도록 영감을주었습니다 https://medium.com/gophersland/gopher-vs-object-oriented-golang-4fa62b88c701


나는 당신이 여기에서 말하는 것의 99 %를 좋아하고 그것에 강하게 동의합니다. 그것은 당신의 예가 당신의 요점을 설명하는 가장 좋은 방법인지 궁금합니다. TokenCache는 본질적으로 맵이 아닙니다 (@VonC에서- "수신자가 맵, func 또는 chan 인 경우 포인터를 사용하지 마십시오"). 맵은 참조 유형이므로 "Add ()"를 포인터 수신기로 만들면 무엇을 얻을 수 있습니까? TokenCache의 모든 사본은 동일한 맵을 참조합니다. Go 플레이 그라운드보기 -play.golang.com/p/Xda1rsGwvhq
Rich

우리가 정렬되어 기쁩니다. 좋은 지적입니다. 사실, 저는이 예제에서 포인터를 사용한 것 같습니다. 왜냐하면 TokenCache가 그 맵보다 더 많은 것을 처리하는 프로젝트에서 복사했기 때문입니다. 그리고 한 가지 방법으로 포인터를 사용하면 모두에서 사용합니다. 이 특정 SO 예제에서 포인터를 제거하는 것이 좋습니다.
Lukas Lukac

LOL, 복사 / 붙여 넣기가 다시 발생합니다! 😉 IMO는 빠지기 쉬운 함정을 보여주기 때문에 그대로 두거나지도를 상태 및 / 또는 큰 데이터 구조를 보여주는 것으로 대체 할 수 있습니다.
Rich

글쎄, 나는 그들이 댓글을 읽을 것이라고 확신합니다 ... 추신 : 리치, 당신의 주장은 합리적입니다. 링크드 인 (내 프로필의 링크)에 저를 추가해주세요.
Lukas Lukac
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.