Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

13장. 구조체

11장과 12장에서 다룬 슬라이스와 맵은 같은 타입의 값을 줄 세우거나 키-값으로 짝지어 보관하는 도구였다.

그런데 실제 프로그램에서는 서로 다른 타입의 값들이 한 덩어리로 묶여 다녀야 하는 경우가 많다.

  • 사용자 한 명 — 이름(string), 나이(int), 활성(bool)
  • 상품 하나 — 코드(string), 가격(int), 재고(int)
  • 좌표 한 점 — X(int), Y(int)

이런 “한 덩어리짜리 데이터” 를 표현하는 도구가 구조체 (struct) 다. 다른 언어의 record, dataclass, 또는 필드만 있는 객체와 비슷하다.

목표:

  • 구조체를 정의하고 만드는 여러 방법 익히기
  • 필드에 접근하고 수정하기
  • 중첩 구조체와 익명 구조체 다루기
  • 구조체 비교가 가능한 조건 이해하기

13.1 구조체란

구조체는 서로 다른 타입의 필드들을 하나의 이름 아래 묶는 사용자 정의 타입이다.

Person
├── Name string
└── Age  int

이렇게 묶어 두면 “사용자 정보” 라는 한 단위로 다룰 수 있다. 함수 인자로 넘기거나, 슬라이스에 담거나, 맵의 값으로 쓰는 일이 쉬워진다.

type Person struct {
    Name string
    Age  int
}

alice := Person{Name: "Alice", Age: 30}

Person 이라는 새 타입이 생겼고, 그 타입의 값 하나가 alice 다.


13.2 구조체 정의

구조체는 type 키워드와 struct 키워드를 함께 써서 정의한다.

type Person struct {
    Name string
    Age  int
}

읽는 법.

  • type Person ... — “Person 이라는 새 타입을 만든다”
  • struct { ... } — “그 타입은 구조체이고, 안에는 다음 필드들이 있다”

필드 작성 규칙

필드 한 줄에 하나씩 쓰는 것이 표준이다. 이름이 먼저, 타입이 뒤에 온다 (변수 선언과 같은 순서).

type Book struct {
    Title    string
    Author   string
    Pages    int
    InStock  bool
}

같은 타입의 필드 여러 개를 한 줄에 묶을 수도 있다.

type Point struct {
    X, Y int
}

XY 둘 다 int 다.

한 줄짜리 정의

세미콜론으로 구분해서 한 줄에 쓰는 것도 문법적으론 가능하다.

type Person struct { Name string; Age int }

하지만 실무에서는 거의 쓰지 않는다. gofmt 가 여러 줄로 풀어 주기도 한다.


13.3 구조체 초기화

값을 채워 새 구조체를 만드는 방법은 크게 세 가지다.

1. 필드 이름 지정 (권장)

alice := Person{
    Name: "Alice",
    Age:  30,
}

각 필드의 이름과 값을 명시한다.

장점.

  • 어떤 값이 어디로 들어가는지 한눈에 보인다
  • 나중에 필드 순서가 바뀌어도 문제 없다
  • 일부 필드만 채워도 된다 (나머지는 제로값)

대부분의 코드에서 이 방식을 쓴다.

2. 순서대로 (비권장)

이름을 빼고 값만 순서대로 나열할 수 있다.

alice := Person{"Alice", 30}

짧지만 함정이 있다. 필드를 하나 추가하거나 순서를 바꾸면 모든 호출 지점이 깨진다.

// 나중에 Email 필드가 추가되면
type Person struct {
    Name  string
    Age   int
    Email string
}

alice := Person{"Alice", 30}  // 컴파일 에러

좌표처럼 필드가 영원히 두 개일 게 분명한 작은 구조체에서만 조심해서 쓴다.

3. 일부 필드만 (나머지는 제로값)

이름 지정 방식에서는 일부만 채워도 된다.

p := Person{Name: "Alice"}
fmt.Println(p)
// {Alice 0}

Age 를 안 적었기 때문에 int 의 제로값인 0 이 들어간다.

필드 타입제로값
int, float640
string""
boolfalse
슬라이스, 맵, 포인터nil

4장의 제로값 개념이 여기서도 그대로 적용된다.

4. 빈 구조체

값을 하나도 안 채우고 만들 수도 있다.

var p Person       // 모든 필드가 제로값
q := Person{}      // 같은 결과

이 둘은 똑같이 동작한다.


13.4 필드 접근

구조체 안의 필드는 점 . 으로 가져온다.

alice := Person{Name: "Alice", Age: 30}

fmt.Println(alice.Name)  // Alice
fmt.Println(alice.Age)   // 30

대입

같은 문법으로 값을 바꿀 수도 있다.

alice.Age = 31
fmt.Println(alice.Age)  // 31

Println 으로 통째로 출력

구조체 전체를 그대로 출력하면 중괄호 안에 값이 나열된다.

fmt.Println(alice)
// {Alice 31}

필드 이름까지 같이 보고 싶다면 %+v 를 쓴다.

fmt.Printf("%+v\n", alice)
// {Name:Alice Age:31}

7장의 포맷 동사가 여기서 다시 유용해진다.


13.5 중첩 구조체

구조체의 필드 타입은 또 다른 구조체일 수도 있다. 이를 중첩 구조체라고 부른다.

type Person struct {
    Name string
    Age  int
}

type Company struct {
    Name  string
    Owner Person
}

Company 안에 Person 이 통째로 들어 있다.

초기화

리터럴을 중첩해서 쓴다.

acme := Company{
    Name: "Acme",
    Owner: Person{
        Name: "Alice",
        Age:  30,
    },
}

접근

점을 이어서 깊이 들어간다.

fmt.Println(acme.Owner.Name)  // Alice

대입도 같은 방식이다.

acme.Owner.Age = 31
fmt.Println(acme.Owner.Age)   // 31

읽다 보면 자연스러워진다. “acme 의 Owner 의 Name” 처럼 문장 그대로 읽으면 된다.

한 번 더 중첩

깊이를 늘려도 같은 방식이다.

type Address struct {
    City    string
    Country string
}

type Person struct {
    Name    string
    Address Address
}

p := Person{
    Name: "Alice",
    Address: Address{
        City:    "Seoul",
        Country: "KR",
    },
}

fmt.Println(p.Address.City)  // Seoul

Go 에는 “임베딩 (embedding)” 이라는 또 다른 중첩 방식이 있다. 필드 이름을 생략하고 타입만 적는 형태인데, 메서드와 인터페이스 개념이 필요해 15~16장에서 다룬다. 지금은 평범한 필드로서의 중첩만 알면 충분하다.


13.6 익명 구조체

한 번만 쓸 구조체라면 굳이 type 으로 이름을 붙일 필요가 없다. 정의와 사용을 한 줄에 합칠 수 있다.

point := struct {
    X, Y int
}{
    X: 1,
    Y: 2,
}

fmt.Println(point.X, point.Y)  // 1 2

읽는 법.

  • struct { X, Y int } — 타입 정의 (이름 없음)
  • {X: 1, Y: 2} — 그 타입의 값 하나

언제 쓰는가

  • 함수 안에서만 잠깐 쓸 작은 데이터 묶음
  • 테스트 코드에서 케이스마다 다른 모양의 데이터를 표현할 때
  • JSON 응답을 임시로 받을 때 (30장 미리보기)
tests := []struct {
    input    int
    expected int
}{
    {1, 2},
    {2, 4},
    {3, 6},
}

for _, tc := range tests {
    if double(tc.input) != tc.expected {
        fmt.Println("실패:", tc.input)
    }
}

테이블 기반 테스트라고 부르는 흔한 패턴이다. 32장에서 본격적으로 다시 만난다.


13.7 구조체 비교

같은 타입의 구조체 두 개를 == 또는 != 로 비교할 수 있을까?

답은 “필드들이 비교 가능하면 된다” 이다.

비교 가능한 경우

모든 필드가 비교 가능한 타입이라면 구조체 자체도 그대로 비교할 수 있다.

type Point struct {
    X, Y int
}

a := Point{1, 2}
b := Point{1, 2}
c := Point{3, 4}

fmt.Println(a == b)  // true
fmt.Println(a == c)  // false

모든 필드 값이 같아야 같다고 판정된다.

비교 불가능한 경우

필드 중에 슬라이스, 맵, 함수처럼 비교가 불가능한 타입이 하나라도 있으면 구조체 전체도 == 로 비교할 수 없다.

type Bag struct {
    Items []string
}

a := Bag{Items: []string{"a"}}
b := Bag{Items: []string{"a"}}

fmt.Println(a == b)  // 컴파일 에러
// invalid operation: a == b
// (struct containing []string cannot be compared)

이유는 11장과 12장에서 본 그대로다. 슬라이스나 맵은 그 자체가 == 를 지원하지 않는다. 그래서 그걸 품은 구조체도 자동으로 비교 불가 상태가 된다.

맵의 키로 쓸 수 있는가

12장의 “키는 비교 가능한 타입이어야 한다” 와 연결된다.

구조체비교 가능?맵 키로 쓸 수 있나?
Point{X, Y int}
Person{Name string, Age int}
Bag{Items []string}아니오아니오

비교 가능한 구조체는 맵의 키로도 쓸 수 있다는 뜻이다. 좌표를 키로 두는 맵 같은 패턴이 그 덕에 깔끔하게 표현된다.

grid := map[Point]string{
    {0, 0}: "원점",
    {1, 1}: "대각선 한 칸",
}

13.8 정리

이 장에서 살펴본 내용:

  • 구조체는 서로 다른 타입의 필드를 한 덩어리로 묶는다
  • type 이름 struct { ... } 형태로 정의한다
  • 초기화는 필드 이름을 명시하는 방식을 쓰는 게 좋다
    • 일부 필드만 채우면 나머지는 제로값
  • 필드 접근과 수정은 점 . 으로 한다
  • 구조체 안에 구조체를 둘 수 있다 (중첩)
  • 이름 없는 익명 구조체를 즉석에서 만들 수도 있다
  • 비교 가능한 필드만 있다면 구조체 전체를 == 로 비교 가능하다
    • 비교 가능한 구조체는 맵 키로도 쓸 수 있다

슬라이스, 맵, 구조체. 이제 일상적인 데이터 모델을 표현하는 세 가지 도구가 모두 모였다.

하지만 한 가지 답답한 점이 곧 보일 것이다. 구조체를 함수에 넘기면 통째로 복사된다. 큰 구조체를 매번 복사하는 건 비효율적이고, 함수 안에서 값을 바꿔도 호출한 쪽에 반영되지 않는다.

이 문제를 풀기 위해 다음 장에서는 포인터 를 본다. “값을 직접 넘기지 말고 위치만 알려 주는” 방식이다.