Go Struct에 대한 심층 분석
Daniel Hayes
Full-Stack Engineer · Leapcell

Go에서 struct
는 데이터를 정의하고 캡슐화하는 데 사용되는 집계 타입입니다. 이는 서로 다른 타입의 필드를 결합할 수 있게 합니다. 구조체는 다른 언어의 클래스와 유사한 사용자 정의 데이터 타입으로 볼 수 있지만, 상속은 지원하지 않습니다. 메서드는 특정 타입(주로 구조체)과 연결된 함수이며, 해당 타입의 인스턴스를 사용하여 호출할 수 있습니다.
구조체 정의 및 초기화
구조체 정의
구조체는 type
및 struct
키워드를 사용하여 정의됩니다. 다음은 간단한 구조체 정의의 예입니다.
type User struct { Username string Email string SignInCount int IsActive bool }
구조체 초기화
구조체는 다양한 방식으로 초기화할 수 있습니다.
필드 이름을 사용한 초기화
user1 := User{ Username: "alice", Email: "alice@example.com", SignInCount: 1, IsActive: true, }
기본값을 사용한 초기화
일부 필드가 지정되지 않은 경우, 해당 타입의 제로 값으로 초기화됩니다.
user2 := User{ Username: "bob", }
이 예에서 Email
은 빈 문자열(""
), SignInCount
는 0
, IsActive
는 false
로 초기화됩니다.
포인터를 사용한 초기화
구조체는 포인터를 사용하여 초기화할 수도 있습니다.
user3 := &User{ Username: "charlie", Email: "charlie@example.com", }
구조체의 메서드와 동작
Go에서 구조체는 데이터를 저장하는 용도뿐만 아니라 메서드를 정의할 수도 있습니다. 이를 통해 구조체는 데이터와 관련된 동작을 캡슐화할 수 있습니다. 아래는 구조체 메서드와 동작에 대한 자세한 설명입니다.
구조체에 대한 메서드 정의
메서드는 수신자(receiver)를 사용하여 정의됩니다. 수신자는 메서드의 첫 번째 매개변수이며, 메서드가 속한 타입을 지정합니다. 수신자는 값 수신자 또는 포인터 수신자가 될 수 있습니다.
값 수신자
값 수신자는 메서드가 호출될 때 구조체의 복사본을 생성하므로, 필드 수정이 원래 구조체에 영향을 주지 않습니다.
type User struct { Username string Email string } func (u User) PrintInfo() { fmt.Printf("Username: %s, Email: %s\n", u.Username, u.Email) }
포인터 수신자
포인터 수신자를 사용하면 메서드가 원래 구조체 필드를 직접 수정할 수 있습니다.
func (u *User) UpdateEmail(newEmail string) { u.Email = newEmail }
메서드 집합 (Method Sets)
Go에서 구조체의 모든 메서드는 해당 구조체의 메서드 집합을 구성합니다. 값 수신자에 대한 메서드 집합에는 값 수신자를 가진 모든 메서드가 포함되며, 포인터 수신자에 대한 메서드 집합에는 포인터 및 값 수신자를 가진 모든 메서드가 포함됩니다.
인터페이스와 구조체 메서드
구조체 메서드는 종종 인터페이스와 함께 사용하여 다형성을 달성합니다. 인터페이스를 정의할 때 구조체가 구현해야 하는 메서드를 지정합니다.
type UserInfo interface { PrintInfo() } // User는 UserInfo 인터페이스를 구현합니다. func (u User) PrintInfo() { fmt.Printf("Username: %s, Email: %s\n", u.Username, u.Email) } func ShowInfo(ui UserInfo) { ui.PrintInfo() }
구조체의 메모리 정렬
Go에서 구조체의 메모리 정렬은 접근 효율성을 높이기 위해 설계되었습니다. 서로 다른 데이터 타입은 특정 정렬 요구 사항을 가지며, 컴파일러는 이러한 요구 사항을 충족하기 위해 구조체 필드 사이에 패딩 바이트를 삽입할 수 있습니다.
메모리 정렬이란 무엇인가요?
메모리 정렬은 메모리의 데이터가 특정 값의 배수인 주소에 위치해야 함을 의미합니다. 데이터 타입의 크기에 따라 정렬 요구 사항이 결정됩니다. 예를 들어, int32
는 4바이트로 정렬되어야 하고, int64
는 8바이트로 정렬되어야 합니다.
왜 메모리 정렬이 필요한가요?
효율적인 메모리 접근은 CPU 성능에 매우 중요합니다. 변수가 올바르게 정렬되지 않으면 CPU가 데이터를 읽거나 쓰기 위해 여러 번 메모리에 접근해야 할 수 있으며, 이는 성능 저하로 이어집니다. 데이터를 정렬함으로써 컴파일러는 효율적인 메모리 접근을 보장합니다.
구조체 메모리 정렬 규칙
- 필드 정렬: 각 필드의 주소는 해당 타입의 정렬 요구 사항을 충족해야 합니다. 컴파일러는 올바른 정렬을 보장하기 위해 필드 사이에 패딩 바이트를 삽입할 수 있습니다.
- 구조체 정렬: 구조체의 크기는 해당 필드들 중 가장 큰 정렬 요구 사항의 배수여야 합니다.
예시:
package main import ( "fmt" "unsafe" ) type Example struct { a int8 // 1 바이트 b int32 // 4 바이트 c int8 // 1 바이트 } func main() { fmt.Println(unsafe.Sizeof(Example{})) }
출력: 12
분석:
a
는int8
이며, 1바이트를 차지하고, 1로 정렬됩니다.b
는int32
이며, 4바이트로 정렬되어야 합니다. 컴파일러는a
와b
사이에 3개의 패딩 바이트를 삽입하여b
의 주소를 4로 정렬합니다.c
는int8
이며, 1바이트가 필요하지만, 구조체의 총 크기는 4(가장 큰 정렬 요구 사항)의 배수여야 합니다. 컴파일러는 끝에 3개의 패딩 바이트를 추가합니다.
메모리 정렬 최적화
구조체 필드를 재배열하여 패딩을 최소화하고 메모리 사용량을 줄일 수 있습니다.
type Optimized struct { b int32 // 4 바이트 a int8 // 1 바이트 c int8 // 1 바이트 }
출력: 8
이 최적화된 버전에서는 b
가 먼저 배치되어 4바이트로 정렬됩니다. a
와 c
는 연속적으로 배치되어 총 크기가 8바이트가 되므로 최적화되지 않은 버전보다 더 간결합니다.
요약
- Go의 구조체 필드는 잠재적인 패딩 바이트와 함께 정렬 요구 사항에 따라 메모리가 할당됩니다.
- 필드 순서를 조정하면 패딩을 최소화하고 메모리 사용량을 최적화할 수 있습니다.
unsafe.Sizeof
를 사용하여 구조체의 실제 메모리 크기를 확인할 수 있습니다.
중첩된 구조체와 컴포지션
Go에서 중첩된 구조체와 컴포지션은 코드 재사용 및 복잡한 데이터 구성을 위한 강력한 도구입니다. 중첩된 구조체를 사용하면 구조체가 다른 구조체를 필드로 포함할 수 있으므로 복잡한 데이터 모델을 만들 수 있습니다. 반면에 컴포지션은 다른 구조체를 포함하여 새로운 구조체를 생성하므로 코드 재사용이 용이합니다.
중첩된 구조체
중첩된 구조체를 사용하면 한 구조체가 다른 구조체를 필드로 포함할 수 있습니다. 이를 통해 데이터 구조를 보다 유연하고 체계적으로 만들 수 있습니다. 다음은 중첩된 구조체의 예입니다.
package main import "fmt" // Address 구조체 정의 type Address struct { City string Country string } // User 구조체 정의, Address 구조체 포함 type User struct { Username string Email string Address Address // 중첩된 구조체 } func main() { // 중첩된 구조체 초기화 user := User{ Username: "alice", Email: "alice@example.com", Address: Address{ City: "New York", Country: "USA", }, } // 중첩된 구조체의 필드에 접근 fmt.Printf("User: %s, Email: %s, City: %s, Country: %s\n", user.Username, user.Email, user.Address.City, user.Address.Country) }
구조체 컴포지션
컴포지션을 사용하면 여러 구조체를 새로운 구조체로 결합하여 코드 재사용을 활성화할 수 있습니다. 컴포지션에서 구조체는 여러 다른 구조체를 필드로 포함할 수 있습니다. 이는 더 복잡한 모델을 구축하고 공통 필드 또는 메서드를 공유하는 데 도움이 됩니다. 다음은 구조체 컴포지션의 예입니다.
package main import "fmt" // Address 구조체 정의 type Address struct { City string Country string } // Profile 구조체 정의 type Profile struct { Age int Bio string } // User 구조체 정의, Address 및 Profile 컴포지션 type User struct { Username string Email string Address Address // Address 구조체 컴포지션 Profile Profile // Profile 구조체 컴포지션 } func main() { // 컴포지션된 구조체 초기화 user := User{ Username: "bob", Email: "bob@example.com", Address: Address{ City: "New York", Country: "USA", }, Profile: Profile{ Age: 25, Bio: "A software developer.", }, } // 컴포지션된 구조체의 필드에 접근 fmt.Printf("User: %s, Email: %s, City: %s, Age: %d, Bio: %s\n", user.Username, user.Email, user.Address.City, user.Profile.Age, user.Profile.Bio) }
중첩된 구조체와 컴포지션의 차이점
- 중첩된 구조체: 구조체를 함께 결합하는 데 사용되며, 한 구조체의 필드 타입이 다른 구조체입니다. 이 접근 방식은 종종 계층적 관계가 있는 데이터 모델을 설명하는 데 사용됩니다.
- 컴포지션: 구조체가 여러 다른 구조체의 필드를 포함할 수 있도록 합니다. 이 메서드는 코드 재사용을 달성하는 데 사용되며, 구조체가 더 복잡한 동작과 속성을 갖도록 합니다.
요약
중첩된 구조체와 컴포지션은 복잡한 데이터 구조를 구성하고 관리하는 데 도움이 되는 Go의 강력한 기능입니다. 데이터 모델을 설계할 때 중첩된 구조체와 컴포지션을 적절히 사용하면 코드를 더 명확하고 유지 관리하기 쉽게 만들 수 있습니다.
빈 구조체
Go에서 빈 구조체는 필드가 없는 구조체입니다.
크기 및 메모리 주소
빈 구조체는 메모리에서 0바이트를 차지합니다. 그러나 메모리 주소는 상황에 따라 같거나 다를 수 있습니다. 메모리 이스케이프가 발생하면 주소가 동일하며 runtime.zerobase
를 가리킵니다.
// empty_struct.go type Empty struct{} //go:linkname zerobase runtime.zerobase var zerobase uintptr // go:linkname 지시문을 사용하여 zerobase를 runtime.zerobase에 연결합니다. func main() { a := Empty{} b := struct{}{} fmt.Println(unsafe.Sizeof(a) == 0) // true fmt.Println(unsafe.Sizeof(b) == 0) // true fmt.Printf("%p\n", &a) // 0x590d00 fmt.Printf("%p\n", &b) // 0x590d00 fmt.Printf("%p\n", &zerobase) // 0x590d00 c := new(Empty) d := new(Empty) // c와 d를 강제로 이스케이프합니다. fmt.Sprint(c, d) println(c) // 0x590d00 println(d) // 0x590d00 fmt.Println(c == d) // true e := new(Empty) f := new(Empty) println(e) // 0xc00008ef47 println(f) // 0xc00008ef47 fmt.Println(e == f) // false }
출력에서 변수 a
, b
및 zerobase
는 동일한 주소를 공유하며, 모두 전역 변수 runtime.zerobase
(runtime/malloc.go
)를 가리킵니다.
이스케이프 시나리오 관련:
- 변수
c
와d
는 힙으로 이스케이프됩니다. 이들의 주소는0x590d00
이며, 같다고 비교됩니다(true
). - 변수
e
와f
는 주소가 다르며(0xc00008ef47
), 같지 않다고 비교됩니다(false
).
이러한 동작은 Go에서 의도적인 것입니다. 빈 구조체 변수가 이스케이프하지 않으면 포인터는 같지 않습니다. 이스케이프한 후에는 포인터가 같아집니다.
빈 구조체를 포함할 때의 공간 계산
빈 구조체 자체는 공간을 차지하지 않지만, 다른 구조체에 포함될 때는 위치에 따라 공간을 소비할 수 있습니다.
- 구조체의 유일한 필드인 경우, 구조체는 공간을 차지하지 않습니다.
- 첫 번째 또는 중간 필드인 경우, 공간을 차지하지 않습니다.
- 마지막 필드인 경우, 이전 필드와 동일한 공간을 차지합니다.
type s1 struct { a struct{} } type s2 struct { _ struct{} } type s3 struct { a struct{} b byte } type s4 struct { a struct{} b int64 } type s5 struct { a byte b struct{} c int64 } type s6 struct { a byte b struct{} } type s7 struct { a int64 b struct{} } type s8 struct { a struct{} b struct{} } func main() { fmt.Println(unsafe.Sizeof(s1{})) // 0 fmt.Println(unsafe.Sizeof(s2{})) // 0 fmt.Println(unsafe.Sizeof(s3{})) // 1 fmt.Println(unsafe.Sizeof(s4{})) // 8 fmt.Println(unsafe.Sizeof(s5{})) // 16 fmt.Println(unsafe.Sizeof(s6{})) // 2 fmt.Println(unsafe.Sizeof(s7{})) // 16 fmt.Println(unsafe.Sizeof(s8{})) // 0 }
빈 구조체가 배열 또는 슬라이스의 요소인 경우:
var a [10]int fmt.Println(unsafe.Sizeof(a)) // 80 var b [10]struct{} fmt.Println(unsafe.Sizeof(b)) // 0 var c = make([]struct{}, 10) fmt.Println(unsafe.Sizeof(c)) // 24, 슬라이스 헤더의 크기
응용 분야
빈 구조체의 0 크기 속성을 사용하면 추가 메모리 오버헤드 없이 다양한 용도로 사용할 수 있습니다.
키가 없는 구조체 초기화 방지
type MustKeyedStruct struct { Name string Age int _ struct{} } func main() { person := MustKeyedStruct{Name: "hello", Age: 10} fmt.Println(person) person2 := MustKeyedStruct{"hello", 10} // 컴파일 오류: MustKeyedStruct{...}에 값이 너무 적습니다. fmt.Println(person2) }
집합 데이터 구조 구현
package main import ( "fmt" ) type Set struct { items map[interface{}]emptyItem } type emptyItem struct{} var itemExists = emptyItem{} func NewSet() *Set { return &Set{items: make(map[interface{}]emptyItem)} } func (set *Set) Add(item interface{}) { set.items[item] = itemExists } func (set *Set) Remove(item interface{}) { delete(set.items, item) } func (set *Set) Contains(item interface{}) bool { _, contains := set.items[item] return contains } func (set *Set) Size() int { return len(set.items) } func main() { set := NewSet() set.Add("hello") set.Add("world") fmt.Println(set.Contains("hello")) fmt.Println(set.Contains("Hello")) fmt.Println(set.Size()) }
채널을 통한 신호 전송
때로는 채널을 통해 전송되는 데이터의 내용이 관련이 없으며 신호 역할만 합니다. 예를 들어, 빈 구조체는 세마포어 구현에 사용될 수 있습니다.
var empty = struct{}{} type Semaphore chan struct{} func (s Semaphore) P(n int) { for i := 0; i < n; i++ { s <- empty } } func (s Semaphore) V(n int) { for i := 0; i < n; i++ { <-s } } func (s Semaphore) Lock() { s.P(1) } func (s Semaphore) Unlock() { s.V(1) } func NewSemaphore(N int) Semaphore { return make(Semaphore, N) }
저희는 클라우드에 Go 프로젝트를 배포하는 최고의 선택지인 Leapcell입니다.
Leapcell은 웹 호스팅, 비동기 작업 및 Redis를 위한 차세대 서버리스 플랫폼입니다.
- 다국어 지원
- JavaScript, Python, Go 또는 Rust로 개발하세요.
- 무제한 프로젝트를 무료로 배포
- 사용량에 대해서만 지불하세요. 요청도 없고, 요금도 없습니다.
- 탁월한 비용 효율성
- 사용한 만큼만 지불하고, 유휴 요금은 없습니다.
- 예시: 25달러로 평균 응답 시간 60ms에서 694만 건의 요청을 지원합니다.
- 간소화된 개발자 경험
- 간편한 설정을 위한 직관적인 UI
- 완전 자동화된 CI/CD 파이프라인 및 GitOps 통합
- 실행 가능한 통찰력을 위한 실시간 메트릭 및 로깅
- 손쉬운 확장성 및 고성능
- 고도의 동시성을 쉽게 처리하기 위한 자동 확장
- 운영 오버헤드가 없으므로 구축에만 집중하세요.
문서!에서 자세히 알아보세요.
X에서 저희를 팔로우하세요: @LeapcellHQ