[function] 값 수신기 대 포인터 수신기

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

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

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



답변

참고 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) {
...
}


답변

@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


답변