구조체 정의하고 만들기
구조체 타입은 이름 있는 필드의 목록입니다. type Name struct { ... }로 선언하고, 복합 리터럴로 값을 만듭니다.
출력:
Ana 31
{Name:Ana Email:ana@example.com Age:32 Admin:false}
{Name: Email: Age:0 Admin:false}
main.User{Name:"Bob", Email:"", Age:0, Admin:false}
%+v는 필드 이름을, %#v는 Go 문법을 출력합니다. 디버깅하면서 구조체를 살펴보는 가장 빠른 방법입니다.
같은 타입의 필드는 한 줄에 쓸 수 있습니다: X, Y float64. 대문자로 시작하는 필드 이름은 공개되고(다른 패키지와 encoding/json에서 보임), 소문자로 시작하는 이름은 그렇지 않습니다.
리터럴 형태
User{Name: "Ana", Age: 31} // named fields: order-free, omitted fields are zero
User{"Ana", "a@x.com", 31, false} // positional: every field, in order
&User{Name: "Ana"} // pointer to a new struct
new(User) // pointer to a zeroed struct
필드 이름을 쓰는 형태를 권합니다. 위치 기반 리터럴은 누군가 필드를 추가하거나 순서를 바꾸면 깨지고, 다른 패키지의 구조체 타입에 쓰면 go vet이 경고합니다(예: net/url.Error struct literal uses unkeyed fields). Point{1, 2}처럼 아주 작은 지역 타입에는 위치 기반 형태도 괜찮습니다.
기본값 없음: 제로 값과 생성자
Go에는 필드 기본값도 없고 언어 차원의 생성자도 없습니다. 모든 필드는 타입의 제로 값(0, "", false, nil)에서 시작합니다. 두 가지 관용구가 그 빈자리를 채웁니다.
제로 값을 쓸모 있게 만드세요. sync.Mutex, bytes.Buffer, strings.Builder는 모두 초기화 없이 동작합니다. 그렇게 설계할 수 있다면 그렇게 하세요.
필드에 0이 아닌 기본값이 필요하거나 맵처럼 할당해야 하는 것이 있다면 NewX 함수를 작성하세요:
NewServer는 서버에 자신을 수정하는 메서드가 있고 공유될 것이므로 포인터를 반환합니다. 작고 불변인 타입은 구조체를 값으로 반환하는 것도 흔합니다(time.Date는 time.Time을 반환합니다).
구조체 포인터
Go는 필드에 접근할 때 구조체 포인터를 자동으로 역참조합니다. p := &User{}라면 (*p).Name이 아니라 p.Name이라고 씁니다.
출력:
{11 12}
{11 12} {0 12}
{11 0}
구조체는 값입니다. 대입과 함수 인자는 구조체를 복사하고, 포인터는 하나를 공유합니다. 필드 단위 복사는 얕은 복사라서, 복사본의 슬라이스나 맵 필드는 여전히 같은 데이터를 가리킵니다. 무엇을 언제 쓸지는 포인터를 참고하세요.
익명 구조체
구조체 타입은 이름 없이 그 자리에서 쓸 수 있습니다. 일회성 묶음, 테이블 기반 테스트, 한 번만 읽는 JSON 디코딩에 유용합니다.
[]struct{ in string; want int } 테이블은 Go 테스트의 표준 형태입니다.
구조체 비교
모든 필드가 비교 가능하면 구조체는 ==로 비교할 수 있습니다. 같다는 것은 모든 필드가 같다는 뜻입니다. 비교 가능한 구조체는 맵 키로도 쓸 수 있습니다.
슬라이스나 맵 필드가 있는 구조체라면 Equal 메서드를 작성하거나 테스트에서 reflect.DeepEqual로 비교하세요.
구조체 태그
태그는 필드 타입 뒤에 오는 문자열 리터럴입니다. 언어는 태그를 무시하고, 라이브러리가 리플렉션으로 읽습니다. 가장 흔한 사용처는 encoding/json입니다:
이 코드는 {"id":7,"name":"Mug"}를 출력합니다. 태그가 필드 이름을 바꾸고, omitempty가 제로 값인 Price를 빼며, 비공개 필드는 인코더에 보이지 않습니다. 태그는 key:"value" 관례를 따르며, 키가 여럿이면 공백으로 구분합니다: `json:"name" db:"user_name"`. 형식이 잘못된 태그도 문제없이 컴파일되고 조용히 무시되는데, 이것은 go vet이 잡아 줍니다. 옵션은 json 페이지에서 다룹니다.
빈 구조체와 필드 순서
struct{}는 필드가 없고 0바이트를 차지합니다. 집합의 값(map[string]struct{})이나 이벤트만 중요한 채널의 신호(chan struct{})로 씁니다.
필드 순서는 메모리 배치에 영향을 줍니다. 컴파일러가 필드를 정렬 단위에 맞춰 채우기 때문입니다. 64비트 시스템에서 bool, int64, bool로 된 구조체는 24바이트를, int64, bool, bool은 16바이트를 차지합니다. 아주 많이 저장되는 타입에서만 의미가 있습니다.
흔한 실수
- 복사본을 수정함.
for _, u := range users { u.Age++ }는 매번 복사본을 바꿉니다. 인덱스를 쓰세요:users[i].Age++. - 맵 필드 초기화를 잊음. 구조체의 제로 값에는 nil 맵이 들어 있어서 쓰면 패닉이 납니다. 생성자에서 할당하세요.
sync.Mutex를 담은 구조체를 복사함. 복사본은 자기만의 잠금 상태를 갖습니다. 이런 구조체는 포인터로 넘기세요.go vet이 복사를 보고합니다.- 다른 패키지 타입에 위치 기반 리터럴을 씀. 필드가 추가되면 깨집니다.
자주 묻는 질문
Go에서 구조체는 어떻게 초기화하나요?
복합 리터럴을 씁니다. 보통은 필드 이름을 적는 형태입니다: u := User{Name: "Ana", Age: 31}. 생략한 필드는 제로 값을 가집니다. User{}는 모든 필드가 제로 값이고, &User{...}는 포인터를, new(User)는 제로 값 구조체의 포인터를 줍니다.
Go는 구조체 필드의 기본값을 지원하나요?
지원하지 않습니다. 모든 필드는 타입의 제로 값에서 시작합니다. 관용구는 생성자 함수 func NewServer() *Server { return &Server{Port: 8080} }를 쓰거나, sync.Mutex와 bytes.Buffer처럼 제로 값이 이미 쓸모 있도록 타입을 설계하는 것입니다.
Go에서 두 구조체는 어떻게 비교하나요?
모든 필드가 비교 가능하다면 ==로 비교합니다. 대응하는 필드가 모두 같으면 두 구조체 값은 같습니다. 슬라이스, 맵, 함수 필드가 있는 구조체는 ==로 비교할 수 없으므로(컴파일 오류) 필드를 직접 비교하거나 테스트에서 reflect.DeepEqual을 쓰세요.
Go의 구조체 태그란 무엇인가요?
`json:"name,omitempty"`처럼 필드 타입 뒤에 붙이는 문자열 리터럴입니다. 컴파일러는 무시하고, encoding/json, 데이터베이스 드라이버, 검증 라이브러리 같은 패키지가 리플렉션으로 읽어서 필드를 인코딩하거나 이름 붙이거나 검사하는 방식을 정합니다.
Go에서 구조체를 필드 이름과 함께 출력하려면 어떻게 하나요?
fmt.Printf("%+v\n", s)를 쓰면 {Name:Ana Age:31}이 출력됩니다. %v는 값만 출력하고, %#v는 타입 이름을 포함한 Go 문법으로 출력합니다: main.User{Name:"Ana", Age:31}.