15 minute read

개요

사용자 정의 타입은 Go에서 새로운 타입을 생성하는 방법입니다.

주요 특징:

  • 타입 별칭 vs 새 타입: type 키워드로 둘 다 가능
  • 구조체: 여러 필드를 묶은 복합 타입
  • 메서드: 타입에 연결된 함수
  • 임베딩: 상속 대신 조합으로 기능 확장
  • 값 vs 포인터 리시버: 복사 vs 참조 선택
  • 태그: 구조체 필드에 메타데이터 추가
  • 제로값: 모든 타입은 의미 있는 제로값 보유

타입 정의

1. 새로운 타입 정의

package main

import "fmt"

// 기존 타입 기반 새 타입
type Celsius float64
type Fahrenheit float64

// 서로 다른 타입이므로 직접 할당 불가
func typeDefinition() {
    var c Celsius = 25.0
    var f Fahrenheit = 77.0
    
    // ❌ 타입이 다르므로 컴파일 에러
    // c = f
    
    // ✅ 명시적 변환 필요
    c = Celsius(f)
    
    fmt.Printf("%.2f°C\n", c)
}

// 타입별 메서드 정의 가능
func (c Celsius) ToFahrenheit() Fahrenheit {
    return Fahrenheit(c*9/5 + 32)
}

func (f Fahrenheit) ToCelsius() Celsius {
    return Celsius((f - 32) * 5 / 9)
}

func main() {
    c := Celsius(100)
    f := c.ToFahrenheit()
    fmt.Printf("%.2f°C = %.2f°F\n", c, f)
    // 100.00°C = 212.00°F
}

2. 타입 별칭 (Type Alias)

// 타입 별칭 (= 사용)
type MyInt = int

func typeAlias() {
    var a MyInt = 10
    var b int = 20
    
    // ✅ 같은 타입이므로 할당 가능
    a = b
    b = a
    
    fmt.Println(a, b)
}

// 주요 차이점
type NewInt int     // 새로운 타입
type AliasInt = int // int의 별칭

func difference() {
    var n NewInt = 10
    var a AliasInt = 20
    var i int = 30
    
    // NewInt와 int는 다른 타입
    // n = i // 컴파일 에러
    n = NewInt(i) // 변환 필요
    
    // AliasInt와 int는 같은 타입
    a = i // OK
    i = a // OK
}

3. 기본 타입 기반 커스텀 타입

type UserID int64
type Email string
type Age uint8

type Status int

const (
    StatusPending Status = iota
    StatusApproved
    StatusRejected
)

func (s Status) String() string {
    switch s {
    case StatusPending:
        return "Pending"
    case StatusApproved:
        return "Approved"
    case StatusRejected:
        return "Rejected"
    default:
        return "Unknown"
    }
}

func main() {
    userID := UserID(12345)
    email := Email("user@example.com")
    age := Age(25)
    status := StatusApproved
    
    fmt.Printf("User: %d, %s, %d, %s\n", userID, email, age, status)
}

구조체 (Struct)

1. 기본 구조체

type Person struct {
    Name string
    Age  int
    Email string
}

func basicStruct() {
    // 방법 1: 필드 이름 지정
    p1 := Person{
        Name: "Alice",
        Age:  30,
        Email: "alice@example.com",
    }
    
    // 방법 2: 순서대로 초기화 (비권장)
    p2 := Person{"Bob", 25, "bob@example.com"}
    
    // 방법 3: 일부만 초기화 (나머지는 제로값)
    p3 := Person{Name: "Charlie"}
    fmt.Println(p3) // {Charlie 0 }
    
    // 방법 4: var 선언 (모든 필드가 제로값)
    var p4 Person
    fmt.Println(p4) // { 0 }
    
    // 방법 5: new 사용 (포인터 반환)
    p5 := new(Person)
    fmt.Printf("%T\n", p5) // *main.Person
    
    fmt.Println(p1, p2)
}

2. 익명 구조체

func anonymousStruct() {
    // 정의와 동시에 사용
    person := struct {
        Name string
        Age  int
    }{
        Name: "Alice",
        Age:  30,
    }
    
    fmt.Println(person)
    
    // 슬라이스로 사용
    people := []struct {
        Name string
        Age  int
    }{
        {"Alice", 30},
        {"Bob", 25},
        {"Charlie", 35},
    }
    
    for _, p := range people {
        fmt.Printf("%s is %d years old\n", p.Name, p.Age)
    }
}

// 테이블 드리븐 테스트에서 자주 사용
func TestSomething() {
    tests := []struct {
        name     string
        input    int
        expected int
    }{
        {"zero", 0, 0},
        {"positive", 5, 25},
        {"negative", -3, 9},
    }
    
    for _, tt := range tests {
        // 테스트 로직
        _ = tt
    }
}

3. 중첩 구조체

type Address struct {
    Street  string
    City    string
    Country string
}

type Employee struct {
    Name    string
    Age     int
    Address Address // 중첩 구조체
}

func nestedStruct() {
    emp := Employee{
        Name: "Alice",
        Age:  30,
        Address: Address{
            Street:  "123 Main St",
            City:    "Seoul",
            Country: "Korea",
        },
    }
    
    fmt.Println(emp.Address.City) // Seoul
}

4. 구조체 임베딩 (Embedding)

type Animal struct {
    Name string
    Age  int
}

func (a Animal) Speak() {
    fmt.Printf("%s makes a sound\n", a.Name)
}

type Dog struct {
    Animal // 임베딩 (익명 필드)
    Breed  string
}

func (d Dog) Bark() {
    fmt.Printf("%s barks\n", d.Name)
}

func embedding() {
    dog := Dog{
        Animal: Animal{Name: "Buddy", Age: 3},
        Breed:  "Golden Retriever",
    }
    
    // 임베딩된 필드에 직접 접근
    fmt.Println(dog.Name) // Buddy (dog.Animal.Name과 동일)
    
    // 임베딩된 메서드 호출
    dog.Speak() // Buddy makes a sound
    dog.Bark()  // Buddy barks
}

5. 다중 임베딩

type Walker interface {
    Walk()
}

type Swimmer interface {
    Swim()
}

type WalkingAnimal struct {
    Name string
}

func (w WalkingAnimal) Walk() {
    fmt.Printf("%s is walking\n", w.Name)
}

type SwimmingAnimal struct {
    Name string
}

func (s SwimmingAnimal) Swim() {
    fmt.Printf("%s is swimming\n", s.Name)
}

type Duck struct {
    WalkingAnimal
    SwimmingAnimal
}

func multipleEmbedding() {
    duck := Duck{
        WalkingAnimal:  WalkingAnimal{Name: "Donald"},
        SwimmingAnimal: SwimmingAnimal{Name: "Donald"},
    }
    
    duck.Walk() // Donald is walking
    duck.Swim() // Donald is swimming
    
    // ⚠️ 필드 이름 충돌 시 명시적 접근 필요
    // duck.Name // 애매모호하므로 컴파일 에러
    fmt.Println(duck.WalkingAnimal.Name)
    fmt.Println(duck.SwimmingAnimal.Name)
}

구조체 태그 (Struct Tags)

import (
    "encoding/json"
    "fmt"
)

type User struct {
    ID       int    `json:"id"`
    Name     string `json:"name"`
    Email    string `json:"email,omitempty"`
    Password string `json:"-"` // JSON에 포함 안 함
    Age      int    `json:"age,omitempty"`
}

func structTags() {
    user := User{
        ID:       1,
        Name:     "Alice",
        Email:    "alice@example.com",
        Password: "secret123",
        Age:      30,
    }
    
    // JSON 마샬링
    jsonData, _ := json.Marshal(user)
    fmt.Println(string(jsonData))
    // {"id":1,"name":"Alice","email":"alice@example.com","age":30}
    
    // JSON 언마샬링
    jsonStr := `{"id":2,"name":"Bob","email":"bob@example.com"}`
    var user2 User
    json.Unmarshal([]byte(jsonStr), &user2)
    fmt.Printf("%+v\n", user2)
}

태그 활용 예제

import "reflect"

type Product struct {
    Name  string `json:"name" xml:"name" db:"product_name" validate:"required"`
    Price float64 `json:"price" xml:"price" db:"price" validate:"min=0"`
}

func readTags() {
    p := Product{}
    t := reflect.TypeOf(p)
    
    field, _ := t.FieldByName("Name")
    fmt.Println("JSON tag:", field.Tag.Get("json"))     // name
    fmt.Println("DB tag:", field.Tag.Get("db"))         // product_name
    fmt.Println("Validate:", field.Tag.Get("validate")) // required
}

메서드 (Methods)

1. 값 리시버 (Value Receiver)

type Rectangle struct {
    Width  float64
    Height float64
}

// 값 리시버 - 복사본으로 전달
func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

// 값을 변경해도 원본은 안 바뀜
func (r Rectangle) Scale(factor float64) {
    r.Width *= factor
    r.Height *= factor
}

func valueReceiver() {
    rect := Rectangle{Width: 10, Height: 5}
    
    fmt.Println("Area:", rect.Area()) // 50
    
    rect.Scale(2)
    fmt.Println("After scale:", rect.Width) // 10 (변경 안됨!)
}

2. 포인터 리시버 (Pointer Receiver)

type Counter struct {
    count int
}

// 포인터 리시버 - 원본을 수정
func (c *Counter) Increment() {
    c.count++
}

func (c *Counter) Decrement() {
    c.count--
}

func (c *Counter) Value() int {
    return c.count
}

func pointerReceiver() {
    counter := &Counter{}
    
    counter.Increment()
    counter.Increment()
    counter.Decrement()
    
    fmt.Println("Count:", counter.Value()) // 1
    
    // 값으로 선언해도 Go가 자동으로 주소를 전달
    counter2 := Counter{}
    counter2.Increment() // (&counter2).Increment()와 동일
    fmt.Println("Count2:", counter2.Value()) // 1
}

3. 리시버 선택 가이드

// ✅ 포인터 리시버를 사용해야 하는 경우:
type LargeStruct struct {
    data [10000]int
}

// 1. 메서드가 리시버를 수정해야 함
func (l *LargeStruct) Update(index, value int) {
    l.data[index] = value
}

// 2. 구조체가 큼 (복사 비용 절약)
func (l *LargeStruct) Process() {
    // 큰 구조체 복사 피함
}

// 3. 일관성 (일부 메서드가 포인터 리시버면 모두 포인터)
func (l *LargeStruct) Read(index int) int {
    return l.data[index]
}

// ✅ 값 리시버를 사용하는 경우:
type Point struct {
    X, Y int
}

// 1. 작은 구조체 (복사 비용 적음)
// 2. 불변성 보장
// 3. 기본 타입처럼 동작
func (p Point) Distance(other Point) float64 {
    dx := float64(p.X - other.X)
    dy := float64(p.Y - other.Y)
    return math.Sqrt(dx*dx + dy*dy)
}

4. 메서드 체이닝

type QueryBuilder struct {
    table  string
    fields []string
    where  string
    limit  int
}

func NewQueryBuilder() *QueryBuilder {
    return &QueryBuilder{}
}

func (qb *QueryBuilder) Table(name string) *QueryBuilder {
    qb.table = name
    return qb
}

func (qb *QueryBuilder) Select(fields ...string) *QueryBuilder {
    qb.fields = fields
    return qb
}

func (qb *QueryBuilder) Where(condition string) *QueryBuilder {
    qb.where = condition
    return qb
}

func (qb *QueryBuilder) Limit(n int) *QueryBuilder {
    qb.limit = n
    return qb
}

func (qb *QueryBuilder) Build() string {
    return fmt.Sprintf("SELECT %s FROM %s WHERE %s LIMIT %d",
        strings.Join(qb.fields, ", "),
        qb.table,
        qb.where,
        qb.limit)
}

func methodChaining() {
    query := NewQueryBuilder().
        Table("users").
        Select("id", "name", "email").
        Where("age > 18").
        Limit(10).
        Build()
    
    fmt.Println(query)
    // SELECT id, name, email FROM users WHERE age > 18 LIMIT 10
}

Getter와 Setter

type Account struct {
    owner   string
    balance float64
}

// Getter - GetX가 아닌 X 형태 (Go 관례)
func (a *Account) Owner() string {
    return a.owner
}

func (a *Account) Balance() float64 {
    return a.balance
}

// Setter - SetX 형태
func (a *Account) SetOwner(owner string) {
    a.owner = owner
}

func (a *Account) Deposit(amount float64) error {
    if amount <= 0 {
        return fmt.Errorf("amount must be positive")
    }
    a.balance += amount
    return nil
}

func (a *Account) Withdraw(amount float64) error {
    if amount <= 0 {
        return fmt.Errorf("amount must be positive")
    }
    if amount > a.balance {
        return fmt.Errorf("insufficient funds")
    }
    a.balance -= amount
    return nil
}

func getterSetter() {
    account := &Account{owner: "Alice", balance: 1000}
    
    fmt.Println("Owner:", account.Owner())
    fmt.Println("Balance:", account.Balance())
    
    account.Deposit(500)
    fmt.Println("After deposit:", account.Balance()) // 1500
    
    account.Withdraw(200)
    fmt.Println("After withdrawal:", account.Balance()) // 1300
}

구조체 비교

type Point struct {
    X, Y int
}

type Line struct {
    Start, End Point
}

func structComparison() {
    p1 := Point{X: 1, Y: 2}
    p2 := Point{X: 1, Y: 2}
    p3 := Point{X: 3, Y: 4}
    
    // ✅ 모든 필드가 comparable이면 구조체도 비교 가능
    fmt.Println(p1 == p2) // true
    fmt.Println(p1 == p3) // false
    
    l1 := Line{Start: p1, End: p3}
    l2 := Line{Start: p1, End: p3}
    fmt.Println(l1 == l2) // true
}

type NotComparable struct {
    Name string
    Data []int // 슬라이스는 비교 불가능
}

func notComparable() {
    // nc1 := NotComparable{Name: "test", Data: []int{1, 2}}
    // nc2 := NotComparable{Name: "test", Data: []int{1, 2}}
    
    // ❌ 컴파일 에러
    // fmt.Println(nc1 == nc2)
}

구조체 복사

1. 얕은 복사 (Shallow Copy)

type Person struct {
    Name    string
    Age     int
    Hobbies []string // 참조 타입
}

func shallowCopy() {
    p1 := Person{
        Name:    "Alice",
        Age:     30,
        Hobbies: []string{"reading", "coding"},
    }
    
    // 얕은 복사
    p2 := p1
    
    p2.Name = "Bob"
    p2.Age = 25
    
    // 슬라이스는 참조가 복사됨
    p2.Hobbies[0] = "gaming"
    
    fmt.Println(p1.Name)       // Alice (변경 안됨)
    fmt.Println(p1.Hobbies[0]) // gaming (변경됨!)
}

2. 깊은 복사 (Deep Copy)

func (p Person) DeepCopy() Person {
    // 슬라이스 수동 복사
    hobbiesCopy := make([]string, len(p.Hobbies))
    copy(hobbiesCopy, p.Hobbies)
    
    return Person{
        Name:    p.Name,
        Age:     p.Age,
        Hobbies: hobbiesCopy,
    }
}

func deepCopy() {
    p1 := Person{
        Name:    "Alice",
        Age:     30,
        Hobbies: []string{"reading", "coding"},
    }
    
    p2 := p1.DeepCopy()
    p2.Hobbies[0] = "gaming"
    
    fmt.Println(p1.Hobbies[0]) // reading (변경 안됨)
    fmt.Println(p2.Hobbies[0]) // gaming
}

디자인 패턴

1. 생성자 패턴

type User struct {
    id        int
    name      string
    email     string
    createdAt time.Time
}

// 생성자 함수
func NewUser(name, email string) *User {
    return &User{
        id:        generateID(),
        name:      name,
        email:     email,
        createdAt: time.Now(),
    }
}

func generateID() int {
    return rand.Int()
}

func constructor() {
    user := NewUser("Alice", "alice@example.com")
    fmt.Printf("%+v\n", user)
}

2. 옵셔널 패턴 (Functional Options)

type Server struct {
    host    string
    port    int
    timeout time.Duration
    maxConn int
}

type ServerOption func(*Server)

func WithHost(host string) ServerOption {
    return func(s *Server) {
        s.host = host
    }
}

func WithPort(port int) ServerOption {
    return func(s *Server) {
        s.port = port
    }
}

func WithTimeout(timeout time.Duration) ServerOption {
    return func(s *Server) {
        s.timeout = timeout
    }
}

func WithMaxConnections(max int) ServerOption {
    return func(s *Server) {
        s.maxConn = max
    }
}

func NewServer(options ...ServerOption) *Server {
    // 기본값 설정
    server := &Server{
        host:    "localhost",
        port:    8080,
        timeout: 30 * time.Second,
        maxConn: 100,
    }
    
    // 옵션 적용
    for _, option := range options {
        option(server)
    }
    
    return server
}

func functionalOptions() {
    // 기본값 사용
    s1 := NewServer()
    
    // 일부 옵션 지정
    s2 := NewServer(
        WithPort(9000),
        WithTimeout(60*time.Second),
    )
    
    // 모든 옵션 지정
    s3 := NewServer(
        WithHost("0.0.0.0"),
        WithPort(3000),
        WithTimeout(10*time.Second),
        WithMaxConnections(500),
    )
    
    fmt.Printf("%+v\n", s1)
    fmt.Printf("%+v\n", s2)
    fmt.Printf("%+v\n", s3)
}

3. 빌더 패턴

type EmailBuilder struct {
    to      string
    subject string
    body    string
    cc      []string
    bcc     []string
}

func NewEmailBuilder() *EmailBuilder {
    return &EmailBuilder{}
}

func (eb *EmailBuilder) To(to string) *EmailBuilder {
    eb.to = to
    return eb
}

func (eb *EmailBuilder) Subject(subject string) *EmailBuilder {
    eb.subject = subject
    return eb
}

func (eb *EmailBuilder) Body(body string) *EmailBuilder {
    eb.body = body
    return eb
}

func (eb *EmailBuilder) CC(cc ...string) *EmailBuilder {
    eb.cc = append(eb.cc, cc...)
    return eb
}

func (eb *EmailBuilder) BCC(bcc ...string) *EmailBuilder {
    eb.bcc = append(eb.bcc, bcc...)
    return eb
}

func (eb *EmailBuilder) Build() (string, error) {
    if eb.to == "" {
        return "", fmt.Errorf("recipient is required")
    }
    if eb.subject == "" {
        return "", fmt.Errorf("subject is required")
    }
    
    email := fmt.Sprintf("To: %s\nSubject: %s\n", eb.to, eb.subject)
    if len(eb.cc) > 0 {
        email += fmt.Sprintf("CC: %s\n", strings.Join(eb.cc, ", "))
    }
    if len(eb.bcc) > 0 {
        email += fmt.Sprintf("BCC: %s\n", strings.Join(eb.bcc, ", "))
    }
    email += fmt.Sprintf("\n%s", eb.body)
    
    return email, nil
}

func builderPattern() {
    email, err := NewEmailBuilder().
        To("user@example.com").
        Subject("Hello").
        Body("This is a test email").
        CC("cc1@example.com", "cc2@example.com").
        Build()
    
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    
    fmt.Println(email)
}

4. 싱글톤 패턴

import "sync"

type Database struct {
    connection string
}

var (
    instance *Database
    once     sync.Once
)

func GetDatabaseInstance() *Database {
    once.Do(func() {
        fmt.Println("Creating database instance")
        instance = &Database{
            connection: "db://localhost:5432",
        }
    })
    return instance
}

func singleton() {
    db1 := GetDatabaseInstance()
    db2 := GetDatabaseInstance()
    
    fmt.Println(db1 == db2) // true (같은 인스턴스)
}

메모리 레이아웃과 정렬

import "unsafe"

type Compact struct {
    a bool   // 1 byte
    b bool   // 1 byte
    c int32  // 4 bytes
    d int64  // 8 bytes
}

type NotCompact struct {
    a bool   // 1 byte + 7 bytes padding
    d int64  // 8 bytes
    b bool   // 1 byte + 3 bytes padding
    c int32  // 4 bytes
}

func memoryLayout() {
    fmt.Println("Compact size:", unsafe.Sizeof(Compact{}))       // 16 bytes
    fmt.Println("NotCompact size:", unsafe.Sizeof(NotCompact{})) // 24 bytes
    
    // ✅ 필드를 크기순으로 정렬하면 메모리 절약
}

일반적인 실수

1. 값 리시버로 수정 시도

type Counter struct {
    count int
}

// ❌ 값 리시버 - 원본이 변경 안됨
func (c Counter) IncrementBad() {
    c.count++
}

// ✅ 포인터 리시버 - 원본이 변경됨
func (c *Counter) IncrementGood() {
    c.count++
}

func mistake1() {
    c := Counter{}
    
    c.IncrementBad()
    fmt.Println(c.count) // 0 (변경 안됨!)
    
    c.IncrementGood()
    fmt.Println(c.count) // 1 (변경됨)
}

2. 구조체 포인터 nil 체크 누락

type Config struct {
    Timeout int
}

func (c *Config) GetTimeout() int {
    // ❌ nil 체크 없음
    // return c.Timeout // nil일 때 패닉
    
    // ✅ nil 체크
    if c == nil {
        return 30 // 기본값
    }
    return c.Timeout
}

func mistake2() {
    var config *Config
    
    // ❌ nil 포인터 역참조
    // fmt.Println(config.Timeout) // 패닉
    
    // ✅ 메서드에서 nil 처리
    fmt.Println(config.GetTimeout()) // 30
}

3. 구조체 복사 시 참조 타입 공유

type Data struct {
    Values []int
}

func mistake3() {
    d1 := Data{Values: []int{1, 2, 3}}
    
    // ❌ 얕은 복사 - 슬라이스 공유
    d2 := d1
    d2.Values[0] = 99
    
    fmt.Println(d1.Values[0]) // 99 (의도치 않게 변경!)
    
    // ✅ 깊은 복사
    d3 := Data{Values: make([]int, len(d1.Values))}
    copy(d3.Values, d1.Values)
    d3.Values[0] = 100
    
    fmt.Println(d1.Values[0]) // 99 (변경 안됨)
}

4. 임베딩 필드 이름 충돌

type A struct {
    Name string
}

type B struct {
    Name string
}

type C struct {
    A
    B
}

func mistake4() {
    c := C{
        A: A{Name: "A"},
        B: B{Name: "B"},
    }
    
    // ❌ 애매모호함
    // fmt.Println(c.Name) // 컴파일 에러
    
    // ✅ 명시적 접근
    fmt.Println(c.A.Name) // A
    fmt.Println(c.B.Name) // B
}

5. 구조체 비교 시 슬라이스/맵 포함

type Record struct {
    ID   int
    Tags []string
}

func mistake5() {
    r1 := Record{ID: 1, Tags: []string{"a", "b"}}
    r2 := Record{ID: 1, Tags: []string{"a", "b"}}
    
    // ❌ 컴파일 에러 - 슬라이스는 비교 불가
    // fmt.Println(r1 == r2)
    
    // ✅ 수동 비교
    equal := r1.ID == r2.ID &&
        len(r1.Tags) == len(r2.Tags)
    if equal {
        for i := range r1.Tags {
            if r1.Tags[i] != r2.Tags[i] {
                equal = false
                break
            }
        }
    }
    fmt.Println("Equal:", equal)
    
    // ✅ reflect.DeepEqual 사용
    fmt.Println(reflect.DeepEqual(r1, r2))
}

6. 메서드에서 리시버 이름 일관성 없음

type Person struct {
    Name string
}

// ❌ 일관성 없는 리시버 이름
func (person Person) GetName() string {
    return person.Name
}

func (p Person) SetName(name string) {
    p.Name = name
}

// ✅ 일관된 리시버 이름 (보통 타입의 첫 글자 소문자)
func (p Person) Name() string {
    return p.Name
}

7. 구조체 필드 공개/비공개 혼동

package mypackage

type User struct {
    ID       int    // Public (대문자 시작)
    name     string // Private (소문자 시작)
    Email    string // Public
}

func mistake7() {
    user := User{
        ID:    1,
        // name: "Alice", // 같은 패키지에서만 접근 가능
        Email: "alice@example.com",
    }
    
    // JSON 마샬링 시 private 필드는 제외됨
    jsonData, _ := json.Marshal(user)
    fmt.Println(string(jsonData)) // {"ID":1,"Email":"alice@example.com"}
}

구조체 JSON 처리

import (
    "encoding/json"
    "time"
)

type Article struct {
    ID        int       `json:"id"`
    Title     string    `json:"title"`
    Content   string    `json:"content,omitempty"`
    Author    string    `json:"author"`
    Published bool      `json:"published"`
    CreatedAt time.Time `json:"created_at"`
    Tags      []string  `json:"tags,omitempty"`
}

func jsonHandling() {
    article := Article{
        ID:        1,
        Title:     "Go Structs",
        Content:   "Learn about structs in Go",
        Author:    "Alice",
        Published: true,
        CreatedAt: time.Now(),
        Tags:      []string{"go", "programming"},
    }
    
    // 마샬링 (구조체 → JSON)
    jsonData, err := json.MarshalIndent(article, "", "  ")
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    fmt.Println(string(jsonData))
    
    // 언마샬링 (JSON → 구조체)
    jsonStr := `{
        "id": 2,
        "title": "Go Interfaces",
        "author": "Bob",
        "published": false,
        "created_at": "2024-01-01T00:00:00Z"
    }`
    
    var article2 Article
    err = json.Unmarshal([]byte(jsonStr), &article2)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    fmt.Printf("%+v\n", article2)
}

커스텀 JSON 마샬링

type CustomDate time.Time

func (cd CustomDate) MarshalJSON() ([]byte, error) {
    t := time.Time(cd)
    formatted := fmt.Sprintf("\"%s\"", t.Format("2006-01-02"))
    return []byte(formatted), nil
}

func (cd *CustomDate) UnmarshalJSON(data []byte) error {
    str := string(data)
    str = strings.Trim(str, "\"")
    
    t, err := time.Parse("2006-01-02", str)
    if err != nil {
        return err
    }
    
    *cd = CustomDate(t)
    return nil
}

type Event struct {
    Name string     `json:"name"`
    Date CustomDate `json:"date"`
}

func customJSON() {
    event := Event{
        Name: "Conference",
        Date: CustomDate(time.Now()),
    }
    
    jsonData, _ := json.Marshal(event)
    fmt.Println(string(jsonData))
    // {"name":"Conference","date":"2024-01-01"}
}

정리

  • 타입 정의: type NewType BaseType (새 타입) vs type Alias = Type (별칭)
  • 구조체: 여러 필드를 묶은 복합 타입
  • 제로값: var 선언 시 모든 필드가 제로값으로 초기화
  • 임베딩: 상속 대신 조합으로 기능 확장 (익명 필드)
  • 값 리시버: 불변, 작은 구조체에 적합
  • 포인터 리시버: 수정, 큰 구조체에 적합
  • 메서드 체이닝: 포인터 반환으로 체이닝 패턴 구현
  • Getter: X() 형태 (GetX 아님)
  • Setter: SetX() 형태
  • 태그: 구조체 필드에 메타데이터 추가 (JSON, DB, 검증)
  • 비교: 모든 필드가 comparable이면 구조체도 비교 가능
  • 복사: 기본은 얕은 복사, 깊은 복사는 수동 구현 필요
  • 패턴: 생성자, 옵션, 빌더, 싱글톤
  • 메모리 최적화: 필드를 크기순으로 정렬
  • JSON: 태그로 마샬링/언마샬링 제어, 커스텀 구현 가능