Skip to content
Published on

Go 언어 완전 문법 가이드: 기초부터 동시성, 제네릭까지

Authors

소개

Go(Golang)는 Google이 2009년 발표한 정적 타입, 컴파일형 언어입니다. 단순한 문법, 빠른 컴파일, 강력한 동시성 지원, 풍부한 표준 라이브러리가 특징입니다. 클라우드 인프라, 마이크로서비스, CLI 도구 개발에 특히 강력합니다.

이 가이드는 Go의 핵심 문법을 처음부터 끝까지 체계적으로 정리합니다.


1. Go 언어 기초 문법

1.1 변수 선언

Go에서 변수를 선언하는 방법은 여러 가지입니다.

package main

import "fmt"

func main() {
    // var 키워드로 선언 (타입 명시)
    var name string = "Go"
    var age int = 15

    // 타입 추론 (var)
    var pi = 3.14159

    // 단축 선언 (:=) - 함수 내부에서만 사용 가능
    greeting := "Hello, Go!"
    count := 42

    // 복수 변수 선언
    var x, y int = 10, 20
    a, b := 100, 200

    // 제로값: 선언 시 초기화하지 않으면 타입의 기본값
    var zeroInt int       // 0
    var zeroStr string    // ""
    var zeroBool bool     // false

    fmt.Println(name, age, pi, greeting, count)
    fmt.Println(x, y, a, b)
    fmt.Println(zeroInt, zeroStr, zeroBool)
}

1.2 상수와 iota

package main

import "fmt"

// 단순 상수
const Pi = 3.14159
const MaxSize = 1024

// 상수 블록
const (
    StatusOK    = 200
    StatusNotFound = 404
    StatusError = 500
)

// iota: 열거형 상수 자동 증가
type Direction int

const (
    North Direction = iota // 0
    East                   // 1
    South                  // 2
    West                   // 3
)

type ByteSize float64

const (
    _           = iota // blank identifier로 0 건너뜀
    KB ByteSize = 1 << (10 * iota) // 1 << 10 = 1024
    MB                              // 1 << 20
    GB                              // 1 << 30
)

func main() {
    fmt.Println(North, East, South, West) // 0 1 2 3
    fmt.Printf("KB=%.0f, MB=%.0f, GB=%.0f\n", float64(KB), float64(MB), float64(GB))
}

1.3 기본 타입

package main

import "fmt"

func main() {
    // 정수 타입
    var i8 int8 = 127
    var i16 int16 = 32767
    var i32 int32 = 2147483647
    var i64 int64 = 9223372036854775807
    var ui uint = 42
    var ui64 uint64 = 18446744073709551615

    // 플랫폼 의존 (64비트 시스템에서 int64)
    var n int = 100

    // 부동소수점
    var f32 float32 = 3.14
    var f64 float64 = 3.141592653589793

    // 복소수
    var c128 complex128 = 3 + 4i

    // 문자열 (UTF-8 인코딩, 불변)
    s := "Hello, 세계"
    fmt.Println(len(s))        // 바이트 수
    fmt.Println([]rune(s))     // 룬(유니코드 코드포인트) 슬라이스

    // byte (uint8의 별칭)
    var b byte = 'A'

    // rune (int32의 별칭, 유니코드 코드포인트)
    var r rune = '한'

    // bool
    var flag bool = true

    fmt.Println(i8, i16, i32, i64, ui, ui64, n)
    fmt.Println(f32, f64, c128)
    fmt.Println(b, r, flag)
}

1.4 배열과 슬라이스

package main

import "fmt"

func main() {
    // 배열: 고정 크기, 값 타입
    var arr [5]int
    arr[0] = 10
    arr2 := [3]string{"Go", "Python", "Rust"}
    arr3 := [...]int{1, 2, 3, 4, 5} // 컴파일러가 크기 추론

    fmt.Println(arr, arr2, arr3)

    // 슬라이스: 동적 크기, 참조 타입
    s1 := []int{1, 2, 3}
    s2 := make([]int, 3)       // len=3, cap=3
    s3 := make([]int, 3, 10)   // len=3, cap=10

    fmt.Println(len(s1), cap(s1)) // 3, 3
    fmt.Println(len(s2), cap(s2)) // 3, 3
    fmt.Println(len(s3), cap(s3)) // 3, 10

    // append: 용량 초과 시 새 배열 할당
    s1 = append(s1, 4, 5)
    fmt.Println(s1) // [1 2 3 4 5]

    // 슬라이싱
    s4 := s1[1:3]  // [2 3] (원본과 메모리 공유)
    s5 := s1[2:]   // [3 4 5]
    s6 := s1[:2]   // [1 2]

    fmt.Println(s4, s5, s6)

    // copy: 독립적인 복사본 생성
    dst := make([]int, len(s1))
    n := copy(dst, s1)
    fmt.Printf("복사된 원소 수: %d, dst: %v\n", n, dst)

    // 2차원 슬라이스
    matrix := make([][]int, 3)
    for i := range matrix {
        matrix[i] = make([]int, 3)
        for j := range matrix[i] {
            matrix[i][j] = i*3 + j
        }
    }
    fmt.Println(matrix)
}

1.5 맵

package main

import "fmt"

func main() {
    // 맵 선언과 초기화
    m1 := map[string]int{
        "apple":  5,
        "banana": 3,
        "cherry": 8,
    }

    m2 := make(map[string][]string)

    // 값 추가/수정
    m1["date"] = 12
    m2["fruits"] = []string{"apple", "banana"}

    // 값 읽기
    val := m1["apple"]
    fmt.Println(val) // 5

    // comma-ok 패턴: 키 존재 여부 확인
    val2, ok := m1["mango"]
    if !ok {
        fmt.Println("mango not found, zero value:", val2)
    }

    // 삭제
    delete(m1, "cherry")
    fmt.Println(m1)

    // 순회 (순서 보장 안 됨)
    for key, value := range m1 {
        fmt.Printf("%s: %d\n", key, value)
    }

    // nil 맵에 쓰기는 패닉 발생
    var m3 map[string]int
    // m3["key"] = 1 // panic: assignment to entry in nil map
    _ = m3
}

1.6 포인터

package main

import "fmt"

func increment(n *int) {
    *n++ // 역참조
}

func newInt(val int) *int {
    return &val // 로컬 변수의 주소 반환 가능 (힙 이동)
}

func main() {
    x := 42
    p := &x        // x의 주소
    fmt.Println(p)  // 메모리 주소 출력 (예: 0xc0000b4010)
    fmt.Println(*p) // 42 (역참조)

    *p = 100
    fmt.Println(x) // 100

    increment(&x)
    fmt.Println(x) // 101

    ptr := newInt(200)
    fmt.Println(*ptr) // 200

    // nil 포인터
    var nilPtr *int
    fmt.Println(nilPtr) // <nil>
    // fmt.Println(*nilPtr) // panic: nil pointer dereference
}

1.7 제어문

package main

import "fmt"

func main() {
    // for: Go의 유일한 반복문
    for i := 0; i < 5; i++ {
        fmt.Print(i, " ")
    }
    fmt.Println()

    // while 스타일
    n := 1
    for n < 100 {
        n *= 2
    }
    fmt.Println(n)

    // range
    nums := []int{10, 20, 30, 40}
    for idx, val := range nums {
        fmt.Printf("[%d]=%d ", idx, val)
    }
    fmt.Println()

    // 인덱스만 사용
    for i := range nums {
        fmt.Print(nums[i], " ")
    }
    fmt.Println()

    // switch (break 없이도 fallthrough 안 됨)
    day := "Monday"
    switch day {
    case "Saturday", "Sunday":
        fmt.Println("주말")
    case "Monday":
        fmt.Println("월요일")
    default:
        fmt.Println("평일")
    }

    // 조건 없는 switch (if-else 대체)
    score := 85
    switch {
    case score >= 90:
        fmt.Println("A")
    case score >= 80:
        fmt.Println("B")
    case score >= 70:
        fmt.Println("C")
    default:
        fmt.Println("F")
    }

    // defer: 함수 종료 시 실행 (LIFO 순서)
    fmt.Println("시작")
    defer fmt.Println("첫 번째 defer")
    defer fmt.Println("두 번째 defer")
    fmt.Println("끝")
    // 출력: 시작 -> 끝 -> 두 번째 defer -> 첫 번째 defer
}

2. 함수와 메서드

2.1 다중 반환값과 Named Return

package main

import (
    "errors"
    "fmt"
)

// 다중 반환값
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

// Named return values
func minMax(nums []int) (min, max int) {
    if len(nums) == 0 {
        return // 제로값 반환
    }
    min, max = nums[0], nums[0]
    for _, n := range nums[1:] {
        if n < min {
            min = n
        }
        if n > max {
            max = n
        }
    }
    return // naked return
}

// Variadic functions (가변 인수)
func sum(nums ...int) int {
    total := 0
    for _, n := range nums {
        total += n
    }
    return total
}

func main() {
    result, err := divide(10, 3)
    if err != nil {
        fmt.Println("Error:", err)
    } else {
        fmt.Printf("%.4f\n", result) // 3.3333
    }

    _, err = divide(5, 0)
    if err != nil {
        fmt.Println("Error:", err) // division by zero
    }

    min, max := minMax([]int{3, 1, 4, 1, 5, 9, 2, 6})
    fmt.Println(min, max) // 1 9

    fmt.Println(sum(1, 2, 3, 4, 5)) // 15

    // 슬라이스를 variadic 인수로 펼치기
    nums := []int{10, 20, 30}
    fmt.Println(sum(nums...)) // 60
}

2.2 일급 함수와 클로저

package main

import "fmt"

// 함수를 인수로 받기
func apply(nums []int, fn func(int) int) []int {
    result := make([]int, len(nums))
    for i, n := range nums {
        result[i] = fn(n)
    }
    return result
}

// 함수를 반환하기
func multiplier(factor int) func(int) int {
    return func(n int) int {
        return n * factor
    }
}

// 클로저: 외부 변수를 캡처
func counter() func() int {
    count := 0
    return func() int {
        count++
        return count
    }
}

func main() {
    double := func(n int) int { return n * 2 }
    nums := []int{1, 2, 3, 4, 5}

    fmt.Println(apply(nums, double))    // [2 4 6 8 10]
    fmt.Println(apply(nums, func(n int) int { return n * n })) // [1 4 9 16 25]

    triple := multiplier(3)
    fmt.Println(triple(7)) // 21

    c1 := counter()
    c2 := counter()
    fmt.Println(c1(), c1(), c1()) // 1 2 3
    fmt.Println(c2(), c2())       // 1 2 (독립적인 상태)
}

2.3 메서드 (Value Receiver vs Pointer Receiver)

package main

import (
    "fmt"
    "math"
)

type Circle struct {
    Radius float64
}

// Value receiver: 복사본에 적용 (원본 변경 불가)
func (c Circle) Area() float64 {
    return math.Pi * c.Radius * c.Radius
}

func (c Circle) Perimeter() float64 {
    return 2 * math.Pi * c.Radius
}

// Pointer receiver: 원본 변경 가능
func (c *Circle) Scale(factor float64) {
    c.Radius *= factor
}

type Rectangle struct {
    Width, Height float64
}

func (r *Rectangle) Area() float64 {
    return r.Width * r.Height
}

// init() 함수: 패키지 초기화 시 자동 실행
func init() {
    fmt.Println("패키지 초기화")
}

func main() {
    c := Circle{Radius: 5}
    fmt.Printf("넓이: %.2f\n", c.Area())
    fmt.Printf("둘레: %.2f\n", c.Perimeter())

    c.Scale(2)
    fmt.Printf("스케일 후 반지름: %.1f\n", c.Radius) // 10.0

    // 포인터 메서드를 값에서 호출 가능 (자동 주소 취득)
    c2 := Circle{Radius: 3}
    (&c2).Scale(1.5) // 자동으로 &c2로 변환
}

3. 구조체와 인터페이스

3.1 구조체와 임베딩

package main

import "fmt"

type Animal struct {
    Name string
    Age  int
}

func (a Animal) Speak() string {
    return a.Name + " makes a sound"
}

// 임베딩: 상속이 아닌 구성(Composition)
type Dog struct {
    Animal              // 임베딩 (익명 필드)
    Breed  string
}

func (d Dog) Speak() string {
    return d.Name + " says: Woof!"
}

type ServiceDog struct {
    Dog
    ServiceType string
}

func main() {
    d := Dog{
        Animal: Animal{Name: "Buddy", Age: 3},
        Breed:  "Labrador",
    }

    // 임베딩된 필드 직접 접근
    fmt.Println(d.Name)  // d.Animal.Name과 동일
    fmt.Println(d.Age)
    fmt.Println(d.Breed)
    fmt.Println(d.Speak()) // Dog의 Speak() 호출
    fmt.Println(d.Animal.Speak()) // Animal의 Speak() 명시적 호출

    sd := ServiceDog{
        Dog:         d,
        ServiceType: "Guide",
    }
    fmt.Println(sd.Name)        // sd.Dog.Animal.Name
    fmt.Println(sd.ServiceType)
}

3.2 인터페이스

package main

import (
    "fmt"
    "math"
)

// 인터페이스 정의
type Shape interface {
    Area() float64
    Perimeter() float64
}

type Stringer interface {
    String() string
}

type Circle struct{ Radius float64 }
type Rectangle struct{ Width, Height float64 }

func (c Circle) Area() float64      { return math.Pi * c.Radius * c.Radius }
func (c Circle) Perimeter() float64 { return 2 * math.Pi * c.Radius }
func (c Circle) String() string     { return fmt.Sprintf("Circle(r=%.1f)", c.Radius) }

func (r Rectangle) Area() float64      { return r.Width * r.Height }
func (r Rectangle) Perimeter() float64 { return 2 * (r.Width + r.Height) }
func (r Rectangle) String() string {
    return fmt.Sprintf("Rect(%.1fx%.1f)", r.Width, r.Height)
}

// 인터페이스를 인수로 받는 함수
func printShapeInfo(s Shape) {
    fmt.Printf("넓이: %.2f, 둘레: %.2f\n", s.Area(), s.Perimeter())
}

// 빈 인터페이스 (모든 타입)
func printAny(v interface{}) {
    fmt.Printf("타입: %T, 값: %v\n", v, v)
}

// 타입 어서션
func describe(i interface{}) {
    if s, ok := i.(string); ok {
        fmt.Printf("문자열: %q\n", s)
        return
    }
    if n, ok := i.(int); ok {
        fmt.Printf("정수: %d\n", n)
        return
    }
    fmt.Printf("알 수 없는 타입: %T\n", i)
}

// 타입 스위치
func typeSwitch(v interface{}) string {
    switch x := v.(type) {
    case int:
        return fmt.Sprintf("int: %d", x)
    case string:
        return fmt.Sprintf("string: %q", x)
    case bool:
        return fmt.Sprintf("bool: %v", x)
    case []int:
        return fmt.Sprintf("[]int with %d elements", len(x))
    default:
        return fmt.Sprintf("unknown type: %T", x)
    }
}

func main() {
    shapes := []Shape{
        Circle{Radius: 5},
        Rectangle{Width: 4, Height: 6},
    }

    for _, s := range shapes {
        printShapeInfo(s)
        if str, ok := s.(Stringer); ok {
            fmt.Println(str.String())
        }
    }

    printAny(42)
    printAny("hello")
    printAny([]int{1, 2, 3})

    describe("Go언어")
    describe(2024)

    fmt.Println(typeSwitch(100))
    fmt.Println(typeSwitch("world"))
    fmt.Println(typeSwitch([]int{1, 2}))
}

4. 에러 처리

4.1 에러 타입과 커스텀 에러

package main

import (
    "errors"
    "fmt"
)

// 센티널 에러 (패키지 수준 에러 변수)
var (
    ErrNotFound   = errors.New("not found")
    ErrPermission = errors.New("permission denied")
)

// 커스텀 에러 타입
type ValidationError struct {
    Field   string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation error: %s - %s", e.Field, e.Message)
}

type DatabaseError struct {
    Code    int
    Message string
    Err     error // wrapped error
}

func (e *DatabaseError) Error() string {
    return fmt.Sprintf("database error [%d]: %s", e.Code, e.Message)
}

func (e *DatabaseError) Unwrap() error {
    return e.Err
}

// fmt.Errorf로 에러 래핑
func findUser(id int) error {
    if id <= 0 {
        return fmt.Errorf("findUser: invalid id %d: %w", id, ErrNotFound)
    }
    return nil
}

func getUser(id int) error {
    if err := findUser(id); err != nil {
        return fmt.Errorf("getUser: %w", err)
    }
    return nil
}

func main() {
    // errors.Is: 에러 체인에서 특정 에러 확인
    err := getUser(-1)
    if errors.Is(err, ErrNotFound) {
        fmt.Println("not found 에러:", err)
    }

    // errors.As: 에러 체인에서 특정 타입 추출
    dbErr := &DatabaseError{
        Code:    500,
        Message: "connection failed",
        Err:     ErrPermission,
    }
    wrapped := fmt.Errorf("service error: %w", dbErr)

    var de *DatabaseError
    if errors.As(wrapped, &de) {
        fmt.Printf("DB 에러 코드: %d\n", de.Code)
    }

    if errors.Is(wrapped, ErrPermission) {
        fmt.Println("권한 에러가 체인에 포함됨")
    }

    // 커스텀 에러 생성
    valErr := &ValidationError{Field: "email", Message: "invalid format"}
    fmt.Println(valErr)
}

4.2 panic과 recover

package main

import "fmt"

// recover로 panic 복구
func safeDiv(a, b int) (result int, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("recovered from panic: %v", r)
        }
    }()
    return a / b, nil
}

func mustPositive(n int) int {
    if n <= 0 {
        panic(fmt.Sprintf("값은 양수여야 합니다: %d", n))
    }
    return n
}

func main() {
    result, err := safeDiv(10, 2)
    fmt.Printf("10/2 = %d, err = %v\n", result, err)

    result, err = safeDiv(10, 0)
    fmt.Printf("10/0 = %d, err = %v\n", result, err)

    // 정상적인 panic 사용: 프로그래밍 오류나 복구 불가능한 상황
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("패닉 복구:", r)
        }
    }()
    mustPositive(-5)
}

5. 고루틴과 채널 (동시성)

5.1 고루틴과 WaitGroup

package main

import (
    "fmt"
    "sync"
    "time"
)

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done()
    fmt.Printf("Worker %d 시작\n", id)
    time.Sleep(time.Millisecond * 100)
    fmt.Printf("Worker %d 완료\n", id)
}

func main() {
    var wg sync.WaitGroup

    for i := 1; i <= 5; i++ {
        wg.Add(1)
        go worker(i, &wg)
    }

    wg.Wait()
    fmt.Println("모든 워커 완료")
}

5.2 채널

package main

import (
    "fmt"
    "time"
)

func producer(ch chan<- int, count int) {
    for i := 0; i < count; i++ {
        ch <- i
        time.Sleep(time.Millisecond * 10)
    }
    close(ch)
}

func main() {
    // 언버퍼드 채널 (동기)
    ch1 := make(chan int)
    go func() { ch1 <- 42 }()
    fmt.Println(<-ch1) // 42

    // 버퍼드 채널 (비동기, 버퍼 차기 전까지 블록 안 됨)
    ch2 := make(chan string, 3)
    ch2 <- "first"
    ch2 <- "second"
    ch2 <- "third"
    fmt.Println(<-ch2) // first

    // 채널 순회
    numbers := make(chan int, 10)
    go producer(numbers, 5)

    for n := range numbers {
        fmt.Print(n, " ")
    }
    fmt.Println()

    // select 문: 여러 채널 동시 처리
    ch3 := make(chan string, 1)
    ch4 := make(chan string, 1)

    go func() {
        time.Sleep(time.Millisecond * 50)
        ch3 <- "채널3"
    }()
    go func() {
        time.Sleep(time.Millisecond * 30)
        ch4 <- "채널4"
    }()

    for i := 0; i < 2; i++ {
        select {
        case msg := <-ch3:
            fmt.Println("수신:", msg)
        case msg := <-ch4:
            fmt.Println("수신:", msg)
        }
    }
}

5.3 Mutex와 동기화 프리미티브

package main

import (
    "fmt"
    "sync"
)

type SafeCounter struct {
    mu sync.Mutex
    v  map[string]int
}

func (c *SafeCounter) Inc(key string) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.v[key]++
}

func (c *SafeCounter) Value(key string) int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.v[key]
}

// RWMutex: 읽기는 동시, 쓰기는 독점
type Cache struct {
    mu   sync.RWMutex
    data map[string]string
}

func (c *Cache) Set(key, value string) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.data[key] = value
}

func (c *Cache) Get(key string) (string, bool) {
    c.mu.RLock()
    defer c.mu.RUnlock()
    val, ok := c.data[key]
    return val, ok
}

// sync.Once: 한 번만 실행
var (
    instance *Cache
    once     sync.Once
)

func getInstance() *Cache {
    once.Do(func() {
        instance = &Cache{data: make(map[string]string)}
        fmt.Println("캐시 인스턴스 생성")
    })
    return instance
}

func main() {
    counter := SafeCounter{v: make(map[string]int)}
    var wg sync.WaitGroup

    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            counter.Inc("key")
        }()
    }
    wg.Wait()
    fmt.Println("카운터:", counter.Value("key")) // 1000

    // sync.Map: 동시성 안전한 맵
    var sm sync.Map
    sm.Store("go", "1.21")
    sm.Store("python", "3.12")

    sm.Range(func(key, value interface{}) bool {
        fmt.Printf("%v: %v\n", key, value)
        return true
    })

    c1 := getInstance()
    c2 := getInstance()
    fmt.Println(c1 == c2) // true (같은 인스턴스)
}

5.4 context.Context

package main

import (
    "context"
    "fmt"
    "time"
)

func doWork(ctx context.Context, id int) error {
    select {
    case <-time.After(time.Millisecond * 200):
        fmt.Printf("작업 %d 완료\n", id)
        return nil
    case <-ctx.Done():
        fmt.Printf("작업 %d 취소: %v\n", id, ctx.Err())
        return ctx.Err()
    }
}

func fetchData(ctx context.Context, url string) (string, error) {
    // 컨텍스트에서 값 읽기
    if requestID, ok := ctx.Value("requestID").(string); ok {
        fmt.Printf("요청 ID: %s, URL: %s\n", requestID, url)
    }
    select {
    case <-time.After(time.Millisecond * 100):
        return "data from " + url, nil
    case <-ctx.Done():
        return "", ctx.Err()
    }
}

func main() {
    // WithCancel
    ctx, cancel := context.WithCancel(context.Background())
    go func() {
        time.Sleep(time.Millisecond * 50)
        cancel() // 취소 신호 전송
    }()
    doWork(ctx, 1)

    // WithTimeout
    ctx2, cancel2 := context.WithTimeout(context.Background(), time.Millisecond*150)
    defer cancel2()
    doWork(ctx2, 2)

    // WithDeadline
    deadline := time.Now().Add(time.Millisecond * 300)
    ctx3, cancel3 := context.WithDeadline(context.Background(), deadline)
    defer cancel3()
    doWork(ctx3, 3)

    // WithValue: 요청 범위 데이터 전달
    ctx4 := context.WithValue(context.Background(), "requestID", "req-001")
    data, err := fetchData(ctx4, "https://api.example.com/users")
    if err == nil {
        fmt.Println("데이터:", data)
    }
}

6. 패키지와 모듈

6.1 Go Modules

# 새 모듈 초기화
go mod init github.com/username/myapp

# 의존성 추가
go get github.com/gin-gonic/gin@v1.9.1

# 의존성 정리
go mod tidy

# 벤더 디렉토리 생성
go mod vendor

go.mod 파일 예시:

module github.com/username/myapp

go 1.21

require (
    github.com/gin-gonic/gin v1.9.1
    github.com/stretchr/testify v1.8.4
)

6.2 패키지 구조 베스트 프랙티스

myapp/
├── cmd/
│   └── server/
│       └── main.go          # 진입점
├── internal/                 # 외부 패키지에서 임포트 불가
│   ├── handler/
│   │   └── user.go
│   ├── service/
│   │   └── user.go
│   └── repository/
│       └── user.go
├── pkg/                     # 외부에서 임포트 가능한 라이브러리
│   └── validator/
│       └── validator.go
├── api/                     # API 정의 (OpenAPI, Protobuf)
├── config/
│   └── config.go
├── go.mod
└── go.sum

6.3 주요 표준 라이브러리

package main

import (
    "fmt"
    "os"
    "strings"
    "strconv"
    "time"
    "io"
    "bytes"
)

func main() {
    // strings 패키지
    s := "  Hello, Go World!  "
    fmt.Println(strings.TrimSpace(s))
    fmt.Println(strings.ToUpper(s))
    fmt.Println(strings.Contains(s, "Go"))
    fmt.Println(strings.Replace(s, "Go", "Golang", 1))
    parts := strings.Split("a,b,c,d", ",")
    fmt.Println(parts)
    fmt.Println(strings.Join(parts, " | "))

    // strconv 패키지
    n, _ := strconv.Atoi("42")
    fmt.Println(n + 1)
    str := strconv.Itoa(100)
    fmt.Println(str + " bottles")
    f, _ := strconv.ParseFloat("3.14", 64)
    fmt.Printf("%.2f\n", f)

    // time 패키지
    now := time.Now()
    fmt.Println(now.Format("2006-01-02 15:04:05"))
    fmt.Println(now.Format(time.RFC3339))
    tomorrow := now.Add(24 * time.Hour)
    fmt.Println(tomorrow.Sub(now))

    t, _ := time.Parse("2006-01-02", "2026-03-17")
    fmt.Println(t.Year(), t.Month(), t.Day())

    // os 패키지
    fmt.Println(os.Getenv("HOME"))
    args := os.Args // 커맨드라인 인수

    // io와 bytes
    buf := bytes.NewBufferString("Hello")
    buf.WriteString(", World")
    data, _ := io.ReadAll(buf)
    fmt.Println(string(data))

    _ = args
}

7. 제네릭 (Go 1.18+)

7.1 제네릭 함수와 타입

package main

import (
    "fmt"
    "golang.org/x/exp/constraints"
)

// 타입 파라미터를 사용한 제네릭 함수
func Min[T constraints.Ordered](a, b T) T {
    if a < b {
        return a
    }
    return b
}

func Map[T, U any](slice []T, fn func(T) U) []U {
    result := make([]U, len(slice))
    for i, v := range slice {
        result[i] = fn(v)
    }
    return result
}

func Filter[T any](slice []T, pred func(T) bool) []T {
    var result []T
    for _, v := range slice {
        if pred(v) {
            result = append(result, v)
        }
    }
    return result
}

func Reduce[T, U any](slice []T, initial U, fn func(U, T) U) U {
    acc := initial
    for _, v := range slice {
        acc = fn(acc, v)
    }
    return acc
}

// 제네릭 타입
type Stack[T any] struct {
    items []T
}

func (s *Stack[T]) Push(item T) {
    s.items = append(s.items, item)
}

func (s *Stack[T]) Pop() (T, bool) {
    var zero T
    if len(s.items) == 0 {
        return zero, false
    }
    top := s.items[len(s.items)-1]
    s.items = s.items[:len(s.items)-1]
    return top, true
}

func (s *Stack[T]) Len() int {
    return len(s.items)
}

// comparable 제약: == 연산자 사용 가능
func Contains[T comparable](slice []T, item T) bool {
    for _, v := range slice {
        if v == item {
            return true
        }
    }
    return false
}

// 커스텀 타입 제약
type Number interface {
    ~int | ~int32 | ~int64 | ~float32 | ~float64
}

func Sum[T Number](nums []T) T {
    var total T
    for _, n := range nums {
        total += n
    }
    return total
}

func main() {
    fmt.Println(Min(3, 5))         // 3
    fmt.Println(Min(3.14, 2.71))   // 2.71
    fmt.Println(Min("apple", "banana")) // apple

    nums := []int{1, 2, 3, 4, 5}
    doubled := Map(nums, func(n int) int { return n * 2 })
    fmt.Println(doubled) // [2 4 6 8 10]

    strs := Map(nums, func(n int) string { return fmt.Sprintf("item_%d", n) })
    fmt.Println(strs)

    evens := Filter(nums, func(n int) bool { return n%2 == 0 })
    fmt.Println(evens) // [2 4]

    total := Reduce(nums, 0, func(acc, n int) int { return acc + n })
    fmt.Println(total) // 15

    var s Stack[string]
    s.Push("first")
    s.Push("second")
    s.Push("third")

    for s.Len() > 0 {
        item, _ := s.Pop()
        fmt.Println(item)
    }

    fmt.Println(Contains([]string{"go", "python", "rust"}, "go")) // true
    fmt.Println(Sum([]float64{1.1, 2.2, 3.3})) // 6.6
}

8. 코드 예시: REST API 서버 (net/http)

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "strconv"
    "strings"
    "sync"
)

type User struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

type UserStore struct {
    mu      sync.RWMutex
    users   map[int]User
    nextID  int
}

func NewUserStore() *UserStore {
    return &UserStore{
        users:  make(map[int]User),
        nextID: 1,
    }
}

func (s *UserStore) Create(name, email string) User {
    s.mu.Lock()
    defer s.mu.Unlock()
    user := User{ID: s.nextID, Name: name, Email: email}
    s.users[s.nextID] = user
    s.nextID++
    return user
}

func (s *UserStore) GetAll() []User {
    s.mu.RLock()
    defer s.mu.RUnlock()
    users := make([]User, 0, len(s.users))
    for _, u := range s.users {
        users = append(users, u)
    }
    return users
}

func (s *UserStore) GetByID(id int) (User, bool) {
    s.mu.RLock()
    defer s.mu.RUnlock()
    u, ok := s.users[id]
    return u, ok
}

// JSON 응답 헬퍼
func writeJSON(w http.ResponseWriter, status int, v interface{}) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(v)
}

// 핸들러 팩토리 (클로저로 의존성 주입)
func makeUsersHandler(store *UserStore) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        switch r.Method {
        case http.MethodGet:
            users := store.GetAll()
            writeJSON(w, http.StatusOK, users)

        case http.MethodPost:
            var body struct {
                Name  string `json:"name"`
                Email string `json:"email"`
            }
            if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
                writeJSON(w, http.StatusBadRequest, map[string]string{
                    "error": "invalid request body",
                })
                return
            }
            user := store.Create(body.Name, body.Email)
            writeJSON(w, http.StatusCreated, user)

        default:
            http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
        }
    }
}

func makeUserHandler(store *UserStore) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        // URL에서 ID 추출: /api/users/123
        parts := strings.Split(r.URL.Path, "/")
        if len(parts) < 4 {
            http.Error(w, "Bad Request", http.StatusBadRequest)
            return
        }
        id, err := strconv.Atoi(parts[3])
        if err != nil {
            http.Error(w, "Invalid ID", http.StatusBadRequest)
            return
        }

        user, ok := store.GetByID(id)
        if !ok {
            writeJSON(w, http.StatusNotFound, map[string]string{
                "error": fmt.Sprintf("user %d not found", id),
            })
            return
        }
        writeJSON(w, http.StatusOK, user)
    }
}

// 미들웨어: 로깅
func loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        log.Printf("%s %s", r.Method, r.URL.Path)
        next.ServeHTTP(w, r)
    })
}

func main() {
    store := NewUserStore()

    // 초기 데이터
    store.Create("Alice", "alice@example.com")
    store.Create("Bob", "bob@example.com")

    mux := http.NewServeMux()
    mux.Handle("/api/users", makeUsersHandler(store))
    mux.Handle("/api/users/", makeUserHandler(store))
    mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
        writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
    })

    handler := loggingMiddleware(mux)

    addr := ":8080"
    fmt.Printf("서버 시작: http://localhost%s\n", addr)
    log.Fatal(http.ListenAndServe(addr, handler))
}

9. 퀴즈

퀴즈 1: 슬라이스 동작 이해

다음 코드의 출력 결과는?

a := []int{1, 2, 3, 4, 5}
b := a[1:3]
b[0] = 99
fmt.Println(a)

정답: [1 99 3 4 5]

설명: 슬라이싱으로 만든 ba와 메모리를 공유합니다. b[0]a[1]과 같은 메모리 위치를 가리키므로 b[0] = 99를 하면 a[1]도 99로 변경됩니다.

퀴즈 2: 고루틴과 클로저 함정

다음 코드는 왜 문제가 있고 어떻게 수정해야 하나요?

for i := 0; i < 5; i++ {
    go func() {
        fmt.Println(i)
    }()
}
time.Sleep(time.Second)

정답: 대부분 5를 5번 출력하거나 예측할 수 없는 값을 출력합니다.

설명: 클로저가 변수 i를 캡처할 때 값이 아닌 참조를 캡처합니다. 고루틴이 실행될 때 이미 반복문이 끝나 i가 5가 됩니다. 수정 방법은 두 가지입니다.

1번 방법 - 인수로 전달:

for i := 0; i < 5; i++ {
    go func(n int) {
        fmt.Println(n)
    }(i)
}

2번 방법 - 지역 변수 생성:

for i := 0; i < 5; i++ {
    i := i // 새 변수 i 생성
    go func() {
        fmt.Println(i)
    }()
}
퀴즈 3: 인터페이스와 nil

다음 코드에서 isNil의 반환값은?

type MyError struct{ msg string }
func (e *MyError) Error() string { return e.msg }

func getError() error {
    var err *MyError = nil
    return err
}

func main() {
    err := getError()
    fmt.Println(err == nil)
}

정답: false

설명: Go의 인터페이스는 타입과 값 두 가지 구성요소를 가집니다. getError()*MyError 타입에 nil 값인 인터페이스를 반환합니다. 이 인터페이스는 타입 정보(*MyError)를 가지고 있으므로 error 인터페이스와 비교할 때 nil이 아닙니다. nil error를 올바르게 반환하려면 return nil로 직접 반환해야 합니다.

퀴즈 4: defer 실행 순서

다음 코드의 출력 순서는?

func main() {
    fmt.Println("A")
    defer fmt.Println("defer 1")
    fmt.Println("B")
    defer fmt.Println("defer 2")
    fmt.Println("C")
    defer fmt.Println("defer 3")
    fmt.Println("D")
}

정답: A B C D defer 3 defer 2 defer 1

설명: defer는 LIFO(Last In, First Out) 스택 방식으로 동작합니다. 일반 문장은 선언 순서대로 실행되고, defer된 함수들은 함수 종료 시 역순(나중에 defer된 것이 먼저)으로 실행됩니다.

퀴즈 5: 채널 방향성

다음 함수 시그니처에서 ch chan<- intch <-chan int의 차이는?

정답:

  • chan<- int: 송신 전용 채널 (쓰기만 가능)
  • <-chan int: 수신 전용 채널 (읽기만 가능)

설명: Go는 채널의 방향을 타입으로 제한할 수 있습니다. 이를 통해 함수가 채널을 어떻게 사용하는지 컴파일 타임에 검증할 수 있습니다. 예시:

func send(ch chan<- int, val int) {
    ch <- val  // OK
    // <-ch    // 컴파일 오류
}

func receive(ch <-chan int) int {
    return <-ch  // OK
    // ch <- 1   // 컴파일 오류
}

func pipe(in <-chan int, out chan<- int) {
    for v := range in {
        out <- v * 2
    }
}

채널 방향을 명시하면 실수로 잘못된 방향으로 채널을 사용하는 것을 방지합니다.