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 에서 사용되는 패턴 )?
답변
tl; dr :
- 수신기 포인터를 사용하는 방법이 일반적입니다. 리시버의 경험 법칙은 “의심 스럽다면 포인터를 사용하십시오”입니다.
- 슬라이스, 맵, 채널, 문자열, 함수 값 및 인터페이스 값은 내부적으로 포인터로 구현되며 해당 포인터는 종종 중복됩니다.
- 다른 곳에서는 큰 구조체 또는 구조체에 포인터를 사용하고 변경해야하며 그렇지 않으면 값을 전달합니다 .
포인터를 자주 사용해야하는 경우 :
- 수신자 는 다른 주장보다 더 자주 포인터입니다. 메소드가 호출 된 것을 수정하거나 명명 된 유형이 큰 구조체 가 되는 것은 드문 일이 아니므 로 드문 경우를 제외하고 지침이 포인터로 기본 설정됩니다.
- Jeff Hodges의 카피 파이터 도구는 가치가없는 비 수신 리시버를 자동으로 검색합니다.
포인터가 필요하지 않은 상황 :
-
코드 리뷰 가이드 라인이 통과하는 것이 좋습니다 작은 구조체를 같은
type Point struct { latitude, longitude float64 }
당신의 요구를 호출하고 함수가 장소에 수정할 수 있도록하지 않는 한 값으로, 어쩌면 조금 더 큰 심지어 일을합니다.- 값 의미론은 여기에 할당이 놀랍게도 값을 변경하는 앨리어싱 상황을 피합니다.
- 약간의 속도로 깨끗한 의미를 희생하는 것은 Go-y가 아니며 때로는 캐시 누락 이나 힙 할당을 피하기 때문에 값으로 작은 구조체를 전달하는 것이 실제로 더 효율적 입니다.
- 따라서 Go Wiki의 코드 검토 주석 페이지에는 구조체가 작고 그 상태를 유지할 가능성이있을 때 값을 기준으로 전달하는 것이 좋습니다.
- “큰”컷오프가 모호해 보이는 경우입니다. 논란의 여지없이 많은 구조체가 포인터 또는 값이 올바른 범위에 있습니다. 하한선으로, 코드 검토 의견은 슬라이스 (3 개의 기계어)가 가치 수신자로 사용하기에 합리적이라고 제안합니다. 상한에 가까울수록
bytes.Replace
10 단어의 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를 강요하지 않는 방법을 찾고 싶습니다.
- 할당을 피하기 위해 Go의 탈출 분석 은 친구입니다. 때로는 간단한 생성자, 일반 리터럴 또는 유용한 0 값으로 초기화 할 수있는 유형을 만들어 힙 할당을 피하는 데 도움이 될 수 있습니다
bytes.Buffer
. Reset()
일부 stdlib 유형이 제공하는 것처럼 오브젝트를 공백 상태로 되 돌리는 방법을 고려하십시오 . 할당을 신경 쓰지 않거나 저장할 수없는 사용자는 할당 할 필요가 없습니다.- 편의를 위해 다음 위치에서
existingUser.LoadFromJSON(json []byte) error
랩핑 될 수있는 위치에서 수정 메소드 및 스크래치에서 작성 기능을 일치하는 쌍 으로 작성하십시오NewUserFromJSON(json []byte) (*User, error)
. 다시 말하지만, 게으름과 곤란한 할당 사이의 선택을 개별 발신자에게 푸시합니다. - 메모리를 재활용하려는 발신자는
sync.Pool
세부 정보를 처리 할 수 있습니다. 특정 할당이 많은 메모리 압력을 발생시키는 경우, alloc이 더 이상 사용되지 않을 때를 잘 알고 있으며 더 나은 최적화를 제공하지 않으면sync.Pool
도움이 될 수 있습니다. (CloudFlare 는 재활용에 대한 유용한 (사전sync.Pool
) 블로그 게시물을 게시했습니다 .)
- 할당을 피하기 위해 Go의 탈출 분석 은 친구입니다. 때로는 간단한 생성자, 일반 리터럴 또는 유용한 0 값으로 초기화 할 수있는 유형을 만들어 힙 할당을 피하는 데 도움이 될 수 있습니다
마지막으로 슬라이스가 포인터 여야하는지 여부에 대해 : 슬라이스 값이 유용 할 수 있으며 할당 및 캐시 미스를 절약 할 수 있습니다. 차단제가있을 수 있습니다.
- 아이템을 생성하는 API는 포인터를 강제 할 수 있습니다. 예를 들어
NewFoo() *Foo
Go를 0으로 초기화하지 않고 호출해야 합니다 . - 항목의 원하는 수명이 모두 같지 않을 수 있습니다. 전체 슬라이스가 한 번에 해제됩니다. 항목의 99 %가 더 이상 유용하지 않지만 다른 1 %에 대한 포인터가 있으면 모든 배열이 할당 된 상태로 유지됩니다.
- 물건을 옮기면 문제가 발생할 수 있습니다. 특히 기본 배열
append
이 커질 때 항목을 복사합니다 . 당신이append
지적 하기 전에 포인터 가 잘못된 곳으로 향하는 포인터는 거대한 구조체의 경우 복사 속도가 느려질 수 있습니다sync.Mutex
. 예를 들어 복사는 허용되지 않습니다. 가운데에 삽입 / 삭제하고 비슷한 방식으로 항목을 이동합니다.
대체로 가치 분할은 모든 항목을 미리 가져 와서 움직이지 않는 경우 (예 : append
초기 설정 후 더 이상 사용하지 않는 경우) 또는 계속 움직여도 확실하지만 확실합니다. 확인 (항목에 대한 포인터 사용 /주의, 효율적으로 복사 할 수있는 작은 크기 등) 때로는 상황의 세부 사항을 생각하거나 측정해야하지만, 이는 대략적인 지침입니다.
답변
메소드 리시버를 포인터로 사용하려는 세 가지 주요 이유 :
-
“먼저 가장 중요한 방법은 수신기를 수정해야합니까? 그렇다면 수신기는 포인터 여야합니다.”
-
“두 번째는 효율성을 고려하는 것이다. 예를 들어 수신기가 큰 경우, 예를 들어 큰 구조라면 포인터 수신기를 사용하는 것이 훨씬 저렴하다.”
-
“다음은 일관성입니다. 유형의 일부 메소드에 포인터 리시버가 있어야하는 경우 나머지도 마찬가지이므로 유형 사용 방법에 관계없이 메소드 세트가 일관됩니다”
참조 : https://golang.org/doc/faq#methods_on_values_or_pointers
편집 : 또 다른 중요한 것은 함수에 전송하는 실제 “유형”을 아는 것입니다. 유형은 ‘값 유형’또는 ‘참조 유형’일 수 있습니다.
슬라이스와 맵이 참조로 작동하더라도 함수에서 슬라이스의 길이를 변경하는 것과 같은 시나리오에서이를 포인터로 전달할 수 있습니다.
답변
일반적으로 포인터를 반환해야하는 경우는 상태 저장 또는 공유 가능 리소스 의 인스턴스 를 구성 할 때 입니다. 이것은 종종 접두사가 붙은 함수에 의해 수행됩니다 .New
그것들은 무언가의 특정 인스턴스를 나타내며 어떤 활동을 조정해야 할 수도 있기 때문에 동일한 자원을 나타내는 복제 / 복사 구조를 생성하는 것은 의미가 없습니다. 따라서 반환 된 포인터는 자원 자체에 대한 핸들 역할을합니다 .
몇 가지 예 :
func NewTLSServer(handler http.Handler) *Server
-테스트를 위해 웹 서버를 인스턴스화func Open(name string) (*File, error)
-파일 접근 핸들을 반환
다른 경우에는 구조가 기본적으로 복사하기에 너무 커서 포인터가 리턴됩니다.
func NewRGBA(r Rectangle) *RGBA
-메모리에 이미지를 할당
또는 포인터를 내부에 포함하는 구조의 복사본을 대신 반환하여 포인터를 직접 반환하지 않아도 될 수 있지만 이것은 관용으로 간주되지 않을 수 있습니다.
- 표준 라이브러리에는 그러한 예제가 없습니다 …
- 관련 질문 : 포인터 또는 값으로 Go에 임베드
답변
가능하면 (예 : 참조로 전달할 필요가없는 비공유 자원) 값을 사용하십시오. 다음과 같은 이유로 :
- 포인터 연산자와 null 검사를 피하면서 코드가 더 훌륭하고 읽기 쉽습니다.
- Null 포인터 패닉에 대비하여 코드가 더 안전합니다.
- 코드가 더 빠를 것입니다. 예, 빠릅니다! 왜?
이유 1 : 스택에 적은 수의 항목을 할당합니다. 스택에서 할당 / 할당 해제는 즉시 이루어 지지만 힙에 할당 / 할당 해제는 비용이 많이들 수 있습니다 (할당 시간 + 가비지 수집). 당신은 여기에 몇 가지 기본 숫자를 볼 수 있습니다 : http://www.macias.info/entry/201802102230_go_values_vs_references.md
이유 2 : 특히 반환 된 값을 슬라이스로 저장하면 메모리 객체가 메모리에 더 압축됩니다. 모든 항목이 인접한 곳에서 슬라이스를 반복하는 것이 모든 항목이 메모리의 다른 부분을 가리키는 슬라이스를 반복하는 것보다 훨씬 빠릅니다. . 간접 단계가 아니라 캐시 미스 증가를위한 것입니다.
신화 차단기 : 일반적인 x86 캐시 라인은 64 바이트입니다. 대부분의 구조체는 그것보다 작습니다. 메모리에 캐시 라인을 복사하는 시간은 포인터를 복사하는 것과 유사합니다.
코드의 중요한 부분이 느린 경우에만 미세 최적화를 시도하고 가독성과 유지 보수가 덜한 비용으로 포인터를 사용하면 속도가 다소 향상되는지 확인합니다.