Go에서는 구조체(struct)가 기본적으로 값에 의해 전달되고 반환됩니다. 이는 함수 호출이나 반환 시 전체 구조체가 복사된다는 것을 의미합니다. 작은 구조체의 경우 이는 투명하지만, 큰 구조체의 경우에는 문제가 됩니다.
처음에 Go는 적은 수의 할당으로 효율적으로 작동하도록 설계되었습니다. 그러나 많은 필드와 중첩된 객체를 사용하는 구조체가 있을 때 큰 데이터를 무의식적으로 복사하는 위험이 발생했습니다. 이러한 작업의 성능은 저하될 수 있으며, 때때로 차이는 프로파일링이나 GC의 고통에서만 드러납니다.
구조체가 큰 크기를 가지면, 매 호출 시 함수, 반환 또는 할당 시 복사하는 것이 비용이 많이 듭니다. 이는 다음과 같은 결과를 초래합니다:
큰 구조체의 경우 구조체의 포인터(*T)를 전달하고 반환하는 것이 좋습니다. 이는 비용을 줄이고 단일 데이터 인스턴스를 활용할 수 있게 합니다.
코드 예시:
package main import "fmt" type Large struct { Data [1024]int } // 값에 의한 전달 (큰 객체에 대해 부적합) func ValueProcess(l Large) { l.Data[0] = 123 // 복사본만 변경됨 } // 포인터에 의한 전달 func PointerProcess(l *Large) { l.Data[0] = 456 // 원본 변경 } func main() { a := Large{} ValueProcess(a) fmt.Println("ValueProcess 이후:", a.Data[0]) // 0 PointerProcess(&a) fmt.Println("PointerProcess 이후:", a.Data[0]) // 456 }
주요 특징:
1. Go에서 함수에서 구조체의 지역 변수에 대한 포인터를 반환할 수 있나요?
네. Go는 포인터가 유효하도록 보장하며, 반환된 포인터가 가리키는 값들을 자동으로 힙으로 이동시킵니다 (escape to heap).
func NewLarge() *Large { l := Large{} return &l }
2. 구조체를 값으로 전달하고 내부 필드를 변경하면 원본이 변경되나요?
아니요: 복사본만 변경되고, 함수 밖의 원본은 그대로 유지됩니다.
3. 항상 구조체에 포인터를 사용해야 하나요?
아니요. 필드 수가 적은 작은 구조체의 경우 값으로 전달하는 것이 안전하고 종종 더 바람직합니다 (immutable/value-semantic), 할당을 절약하고 GC의 부하를 줄입니다.
로깅 서비스에서 각 이벤트는 큰 구조체로 표현되었으며, 함수에서 값으로 반환되었습니다 — 각 변경 사항이 전체 구조체를 복사했습니다.
장점:
단점:
구조체를 포인터로 전달하고 반환하는 것으로 전환하여 func(l *Large) 및 func() *Large와 같은 시그니처를 통해 데이터를 변경했습니다.
장점:
단점: