매개 변수 및 리턴 값의 포인터 대 값


328

Go에는 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
}

나는 이것들의 차이점을 이해합니다. 첫 번째는 구조체의 복사본을 반환하고 두 번째는 함수 내에서 생성 된 구조체 값에 대한 포인터를 반환하고 세 번째는 기존 구조체가 전달되고 값을 재정의합니다.

이 모든 패턴이 다양한 상황에서 사용되는 것을 보았습니다. 이에 관한 모범 사례가 무엇인지 궁금합니다. 언제 사용합니까? 예를 들어, 첫 번째는 작은 구조체에 대해서는 괜찮을 수 있고 (오버 헤드가 최소이므로) 두 번째는 더 큰 구조체에 적합합니다. 세 번째는 메모리간에 매우 효율적으로 사용하려는 경우 호출간에 단일 구조체 인스턴스를 쉽게 재사용 할 수 있기 때문입니다. 사용시기에 대한 모범 사례가 있습니까?

마찬가지로 슬라이스에 관한 동일한 질문 :

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 App Engine API 에서 사용되는 패턴 )?


1
당신이 말했듯이, 그것은 실제로 사용 사례에 달려 있습니다. 상황에 따라 모두 유효합니다-이것은 가변 객체입니까? 사본이나 포인터를 원하십니까? 등등. BTW new(MyStruct):)를 사용하여 언급하지 않았지만 포인터를 할당하고 반환하는 다른 방법에는 실제로 차이가 없습니다.
Not_a_Golfer

15
그것은 말 그대로 공학에 관한 것입니다. 포인터를 반환하면 프로그램 속도가 빨라지도록 Structs가 상당히 커야합니다. 유용한 경우 코드, 프로파일 링, 수정하지 않아도됩니다.
Volker

1
값이나 포인터를 반환하는 유일한 방법은 값이나 포인터를 반환하는 것입니다. 할당 방법은 별도의 문제입니다. 상황에 맞는 것을 사용하고 걱정하기 전에 코드를 작성하십시오.
JimB

3
호기심에서 BTW는 이것을 벤치마킹했습니다. 구조체 대 포인터를 반환하는 속도는 거의 같은 속도 인 것 같지만 포인터를 함수 아래로 전달하는 것은 상당히 빠릅니다. 비록하지 그게 문제 것이라고 수준에서
Not_a_Golfer

1
@ Not_a_Golfer : 함수 외부에서 bc 할당 만 수행한다고 가정합니다. 또한 벤치마킹 값 대 포인터는 구조체의 크기와 메모리 액세스 패턴에 따라 다릅니다. 캐시 라인 크기의 항목을 복사하는 것은 가능한 한 빠르며 CPU 캐시에서 포인터를 역 참조하는 속도는 기본 메모리에서 포인터를 역 참조하는 속도와 크게 다릅니다.
JimB

답변:


392

tl; dr :

  • 수신기 포인터를 사용하는 방법이 일반적입니다. 리시버의 경험 법칙은 "의심 스럽다면 포인터를 사용하십시오"입니다.
  • 슬라이스, 맵, 채널, 문자열, 함수 값 및 인터페이스 값은 내부적으로 포인터로 구현되며 해당 포인터는 종종 중복됩니다.
  • 다른 곳에서는 큰 구조체 또는 구조체에 포인터를 사용하고 변경해야하며 그렇지 않으면 값을 전달합니다 .

포인터를 자주 사용해야하는 경우 :

  • 수신자 는 다른 주장보다 더 자주 포인터입니다. 메소드가 호출 된 것을 수정하거나 명명 된 유형이 큰 구조체 되는 것은 드문 일이 아니므 로 드문 경우를 제외하고 지침이 포인터로 기본 설정됩니다.
    • Jeff Hodges의 카피 파이터 도구는 가치가없는 비 수신 리시버를 자동으로 검색합니다.

포인터가 필요하지 않은 상황 :

  • 코드 리뷰 가이드 라인이 통과하는 것이 좋습니다 작은 구조체를 같은 type Point struct { latitude, longitude float64 }당신의 요구를 호출하고 함수가 장소에 수정할 수 있도록하지 않는 한 값으로, 어쩌면 조금 더 큰 심지어 일을합니다.

    • 값 의미론은 여기에 할당이 놀랍게도 값을 변경하는 앨리어싱 상황을 피합니다.
    • 약간의 속도로 깨끗한 의미를 희생하는 것은 Go-y가 아니며 때로는 캐시 누락 이나 힙 할당을 피하기 때문에 값으로 작은 구조체를 전달하는 것이 실제로 더 효율적 입니다.
    • 따라서 Go Wiki의 코드 검토 주석 페이지에는 구조체가 작고 그 상태를 유지할 가능성이있을 때 값을 기준으로 전달하는 것이 좋습니다.
    • "큰"컷오프가 모호해 보이는 경우입니다. 논란의 여지없이 많은 구조체가 포인터 또는 값이 올바른 범위에 있습니다. 하한선으로, 코드 검토 의견은 슬라이스 (3 개의 기계어)가 가치 수신자로 사용하기에 합리적이라고 제안합니다. 상한에 가까울수록 bytes.Replace10 단어의 args (3 조각 및 int)가 필요합니다.
  • 들어 조각 , 당신은 배열의 변화 요소에 대한 포인터를 통과 할 필요가 없습니다. 예를 들어 io.Reader.Read(p []byte)의 바이트를 변경합니다 p. 그것은 내부적으로 당신이라는 작은 구조의 주위에 통과하고 있기 때문에 ", 값 등의 치료 작은 구조체"틀림없이의 특별한 경우의 슬라이스 헤더 (볼 러스 콕스 (RSC)의 설명 ). 마찬가지로 지도수정하거나 채널에서 통신 하기 위해 포인터가 필요하지 않습니다 .

  • 슬라이스의 경우 슬라이스 의 시작 / 길이 / 용량을 변경하여 append슬라이스 값을 수락하고 새 값을 반환하는 것과 같은 내장 함수를 다시 만듭니다. 나는 그것을 모방 할 것이다. 앨리어싱을 피하고, 새로운 슬라이스를 반환하면 새로운 배열이 할당 될 수 있다는 사실에주의를 기울이고 호출자에게 친숙합니다.

    • 항상 그 패턴을 따르는 것은 아닙니다. 데이터베이스 인터페이스 또는 시리얼 라이저 와 같은 일부 도구 는 컴파일 타임에 유형을 알 수없는 슬라이스에 추가해야합니다. 때로는 interface{}매개 변수 에서 슬라이스에 대한 포인터를 허용합니다 .
  • 슬라이스와 같은 맵, 채널, 문자열 및 함수 및 인터페이스 값 은 내부 참조 또는 이미 참조가 포함 된 구조이므로 기본 데이터를 복사하지 않으려는 경우 포인터를 전달할 필요가 없습니다. . (rsc 는 인터페이스 값이 저장되는 방법에 대한 별도의 게시물을 작성했습니다 ).

    • 당신은 아직도 당신이 원하는 그 희소 한 케이스에 포인터를 전달해야 할 수 있습니다 수정 발신자의 구조체 : flag.StringVar소요 *string예를 들어, 그 이유.

포인터를 사용하는 위치 :

  • 함수가 포인터가 필요한 구조체의 메소드인지 여부를 고려하십시오. 사람들은 많은 방법으로 x수정을 기대 x하므로 수신자가 수정 된 구조체를 만들면 놀라움을 최소화하는 데 도움이 될 수 있습니다. 수신자가 포인터가되어야하는시기 에 대한 지침 이 있습니다.

  • 비 수신자 매개 변수에 영향을 미치는 함수는 godoc 또는 godoc 및 이름 (예 :)에서이를 명확하게해야합니다 reader.WriteTo(writer).

  • 재사용을 허용하여 할당을 피하기 위해 포인터를 수락한다고 언급했습니다. 메모리 재사용을 위해 API를 변경하는 것은 할당이 사소한 비용이 들지 않을 때까지 지연되는 최적화입니다. 그런 다음 모든 사용자에게 까다로운 API를 강요하지 않는 방법을 찾고 싶습니다.

    1. 할당을 피하기 위해 Go의 탈출 분석 은 친구입니다. 때로는 간단한 생성자, 일반 리터럴 또는 유용한 0 값으로 초기화 할 수있는 유형을 만들어 힙 할당을 피하는 데 도움이 될 수 있습니다 bytes.Buffer.
    2. Reset()일부 stdlib 유형이 제공하는 것처럼 오브젝트를 공백 상태로 되 돌리는 방법을 고려하십시오 . 할당을 신경 쓰지 않거나 저장할 수없는 사용자는 할당 할 필요가 없습니다.
    3. 편의를 위해 다음 위치에서 existingUser.LoadFromJSON(json []byte) error랩핑 될 수있는 위치에서 수정 메소드 및 스크래치에서 작성 기능을 일치하는 쌍 으로 작성하십시오 NewUserFromJSON(json []byte) (*User, error). 다시 말하지만, 게으름과 곤란한 할당 사이의 선택을 개별 발신자에게 푸시합니다.
    4. 메모리를 재활용하려는 발신자는 sync.Pool세부 정보를 처리 할 수 있습니다. 특정 할당이 많은 메모리 압력을 발생시키는 경우, alloc이 더 이상 사용되지 않을 때를 잘 알고 있으며 더 나은 최적화를 제공하지 않으면 sync.Pool도움이 될 수 있습니다. (CloudFlare 는 재활용에 대한 유용한 (사전 sync.Pool) 블로그 게시물을 게시했습니다 .)

마지막으로 슬라이스가 포인터 여야하는지 여부에 대해 : 슬라이스 값이 유용 할 수 있으며 할당 및 캐시 미스를 절약 할 수 있습니다. 차단제가있을 수 있습니다.

  • 아이템을 생성하는 API는 포인터를 강제 할 수 있습니다. 예를 들어 NewFoo() *FooGo를 0으로 초기화하지 않고 호출해야 합니다 .
  • 항목의 원하는 수명이 모두 같지 않을 수 있습니다. 전체 슬라이스가 한 번에 해제됩니다. 항목의 99 %가 더 이상 유용하지 않지만 다른 1 %에 대한 포인터가 있으면 모든 배열이 할당 된 상태로 유지됩니다.
  • 물건을 옮기면 문제가 발생할 수 있습니다. 특히 기본 배열append커질 때 항목을 복사합니다 . 당신이 append지적 하기 전에 포인터 가 잘못된 곳으로 향하는 포인터는 거대한 구조체의 경우 복사 속도가 느려질 수 있습니다 sync.Mutex. 예를 들어 복사는 허용되지 않습니다. 가운데에 삽입 / 삭제하고 비슷한 방식으로 항목을 이동합니다.

대체로 가치 분할은 모든 항목을 미리 가져 와서 움직이지 않는 경우 (예 : append초기 설정 후 더 이상 사용하지 않는 경우) 또는 계속 움직여도 확실하지만 확실합니다. 확인 (항목에 대한 포인터 사용 /주의, 효율적으로 복사 할 수있는 작은 크기 등) 때로는 상황의 세부 사항을 생각하거나 측정해야하지만, 이는 대략적인 지침입니다.


12
큰 구조체는 무엇을 의미합니까? 큰 구조체와 작은 구조체의 예가 있습니까?
모자가없는 사용자

1
bytes.Replace는 amd64에서 80 바이트의 args를 사용합니까?
Tim Wu

2
서명은 Replace(s, old, new []byte, n int) []byte; s, old 및 new는 각각 3 워드 ( 슬라이스 헤더는(ptr, len, cap) )이며 n int1 워드이므로 8 바이트 / 워드에서 80 바이트 인 10 워드입니다.
twotwotwo

6
큰 구조체를 어떻게 정의합니까? 얼마나 큰가요?
Andy Aldo

3
@AndyAldo 내 소스 (코드 검토 주석 등) 중 어느 것도 임계 값을 정의하지 않으므로 임계 값을 올리는 대신 판단 호출이라고 결정했습니다. 세 조각 (예 : 슬라이스)은 stdlib에서 가치가있는 것으로 일관되게 처리됩니다. 방금 5 단어 값 수신기 (text / scanner.Position)의 인스턴스를 찾았지만 많이 읽지 않았습니다 (포인터로도 전달됨). 벤치 마크 등이 없으면 가독성에 가장 편리한 것으로 간주됩니다.
twotwotwo

10

메소드 리시버를 포인터로 사용하려는 세 가지 주요 이유 :

  1. "먼저 가장 중요한 방법은 수신기를 수정해야합니까? 그렇다면 수신기는 포인터 여야합니다."

  2. "두 번째는 효율성을 고려하는 것이다. 예를 들어 수신기가 큰 경우, 예를 들어 큰 구조라면 포인터 수신기를 사용하는 것이 훨씬 저렴하다."

  3. "다음은 일관성입니다. 유형의 일부 메소드에 포인터 리시버가 있어야하는 경우 나머지도 마찬가지이므로 유형 사용 방법에 관계없이 메소드 세트가 일관됩니다"

참조 : https://golang.org/doc/faq#methods_on_values_or_pointers

편집 : 또 다른 중요한 것은 함수에 전송하는 실제 "유형"을 아는 것입니다. 유형은 '값 유형'또는 '참조 유형'일 수 있습니다.

슬라이스와 맵이 참조로 작동하더라도 함수에서 슬라이스의 길이를 변경하는 것과 같은 시나리오에서이를 포인터로 전달할 수 있습니다.


1
2의 경우 컷오프는 무엇입니까? 내 구조체가 크거나 작은 지 어떻게 알 수 있습니까? 또한 포인터보다 값을 사용하는 것이 효율적이므로 힙에서 참조 할 필요가없는 작은 구조체가 있습니까?
zlotnika

필드 및 / 또는 중첩 구조체의 수가 많을수록 구조체는 더 커집니다. 구조체가 "큰"또는 "큰"이라고 할 수있는 시점을 알 수있는 특정 컷오프 또는 표준 방법이 있는지 확실하지 않습니다. 구조체를 사용하거나 만들면 위에서 말한 내용에 따라 크거나 작은 지 알 수 있습니다. 하지만 그건 나 뿐이야!.
Santosh Pillai

2

일반적으로 포인터를 반환해야하는 경우는 상태 저장 또는 공유 가능 리소스인스턴스 를 구성 할 때 입니다. 이것은 종종 접두사가 붙은 함수에 의해 수행됩니다 .New

그것들은 무언가의 특정 인스턴스를 나타내며 어떤 활동을 조정해야 할 수도 있기 때문에 동일한 자원을 나타내는 복제 / 복사 구조를 생성하는 것은 의미가 없습니다. 따라서 반환 된 포인터는 자원 자체에 대한 핸들 역할을합니다 .

몇 가지 예 :

다른 경우에는 구조가 기본적으로 복사하기에 너무 커서 포인터가 리턴됩니다.


또는 포인터를 내부에 포함하는 구조의 복사본을 대신 반환하여 포인터를 직접 반환하지 않아도 될 수 있지만 이것은 관용으로 간주되지 않을 수 있습니다.


이 분석에서 기본적으로 구조체는 으로 복사 되지만 반드시 간접 멤버는 아닙니다.
nobar

2

가능하면 (예 : 참조로 전달할 필요가없는 비공유 자원) 값을 사용하십시오. 다음과 같은 이유로 :

  1. 포인터 연산자와 null 검사를 피하면서 코드가 더 훌륭하고 읽기 쉽습니다.
  2. Null 포인터 패닉에 대비하여 코드가 더 안전합니다.
  3. 코드가 더 빠를 것입니다. 예, 빠릅니다! 왜?

이유 1 : 스택에 적은 수의 항목을 할당합니다. 스택에서 할당 / 할당 해제는 즉시 이루어 지지만 힙에 할당 / 할당 해제는 비용이 많이들 수 있습니다 (할당 시간 + 가비지 수집). 당신은 여기에 몇 가지 기본 숫자를 볼 수 있습니다 : http://www.macias.info/entry/201802102230_go_values_vs_references.md

이유 2 : 특히 반환 된 값을 슬라이스로 저장하면 메모리 객체가 메모리에 더 압축됩니다. 모든 항목이 인접한 곳에서 슬라이스를 반복하는 것이 모든 항목이 메모리의 다른 부분을 가리키는 슬라이스를 반복하는 것보다 훨씬 빠릅니다. . 간접 단계가 아니라 캐시 미스 증가를위한 것입니다.

신화 차단기 : 일반적인 x86 캐시 라인은 64 바이트입니다. 대부분의 구조체는 그것보다 작습니다. 메모리에 캐시 라인을 복사하는 시간은 포인터를 복사하는 것과 유사합니다.

코드의 중요한 부분이 느린 경우에만 미세 최적화를 시도하고 가독성과 유지 보수가 덜한 비용으로 포인터를 사용하면 속도가 다소 향상되는지 확인합니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.