Skip to content

Go 치트시트

단순성과 동시성을 위해 구축된 빠르고 정적으로 타입화된 언어.

01

시작하기

Hello World

모든 Go 프로그램은 package main의 main() 함수에서 시작합니다. 실행에는 'go run', 컴파일에는 'go build'를 사용하세요. gofmt가 자동으로 코드 형식을 지정합니다(탭, 간격). import 문은 패키지를 가져옵니다 — fmt는 형식화된 I/O를 처리합니다.

go
package main

import "fmt"

func main() {
    fmt.Println("Hello, World!")
    fmt.Printf("Name: %s, Age: %d\n", "Alice", 30)
}

// Run: go run main.go
// Build: go build -o app main.go
// Format: gofmt -w main.go

Go 모듈(go.mod)

Go 모듈(go.mod)은 Go 1.11부터 의존성을 관리합니다. 'go mod init'이 모듈 파일을 만듭니다. 'go get'이 의존성을 추가합니다. 'go mod tidy'는 사용하지 않는 임포트를 제거하고 누락된 것을 추가합니다. 모듈 경로는 패키지의 임포트 경로입니다.

go
// Initialize a new module
// $ go mod init github.com/user/project

// go.mod file:
module github.com/user/project

go 1.21

require (
    github.com/gin-gonic/gin v1.9.1
    golang.org/x/sync v0.3.0
)

// Add dependencies:
// $ go get github.com/gin-gonic/gin
// $ go mod tidy  // clean up unused deps

패키지 구조

Go는 패키지로 코드를 구성합니다 — 디렉토리당 하나의 패키지. 대문자 이름(Println, Add)은 내보내집니다(public); 소문자 이름(private)은 패키지 전용입니다. 'internal' 디렉토리는 임포트를 부모 모듈로 제한합니다. 패키지 이름은 디렉토리 이름과 일치해야 합니다.

go
// project structure:
// myproject/
//   go.mod
//   main.go          // package main
//   utils/
//     helper.go      // package utils
//     math.go
//   internal/
//     api/           // private (only importable within parent)

// main.go
package main

import (
    "fmt"
    "github.com/user/project/utils"
)

func main() {
    fmt.Println(utils.Add(1, 2))
}

// Exported names start with uppercase letter
// unexported names start with lowercase letter

변수 & 상수

명시적 선언에는 'var name type = value'를, 짧은 선언에는 'name := value'를 사용하세요(함수 내에서만). Go에는 제로 값이 있습니다: 숫자는 0, 문자열은 '', boolean은 false, 포인터/슬라이스/맵은 nil. 상수(const)는 컴파일 타임이며 :=를 사용할 수 없습니다.

go
package main

import "fmt"

// Package-level declarations
var version = "1.0.0" // inferred type
const Pi = 3.14159

func main() {
    // var with type
    var name string = "Alice"
    var age int = 30

    // Short declaration (only inside functions)
    city := "NYC"

    // Multiple assignment
    a, b, c := 1, 2, 3

    // Zero values (default)
    var x int      // 0
    var s string   // ""
    var ok bool    // false
    var p *int     // nil

    fmt.Println(name, age, city, a, b, c, x, s, ok, p)
}

기본 타입 & 변환

Go는 명시적 타입 변환이 필요합니다 — C처럼 암시적 변환이 없습니다. rune은 int32의 별칭입니다(Unicode 코드 포인트). byte는 uint8의 별칭입니다. 숫자 타입 간 변환은 정밀도를 잃을 수 있습니다(float에서 int는 잘라냄). string(65)은 코드 포인트를 문자로 변환합니다.

go
// Numeric types
var i int = 42
var i8 int8 = 127
var u uint = 42
var f32 float32 = 3.14
var f64 float64 = 3.141592653589793

// Other types
var b bool = true
var s string = "hello"
var r rune = 'A' // int32 alias for Unicode code point
var by byte = 255 // uint8 alias

// Type conversion (explicit, no implicit conversion)
var n int = 42
var f float64 = float64(n)  // int -> float64
var s string = string(65)   // 65 -> "A" (rune to string)
var i2 int = int(f)         // float64 -> int (truncates)

// Type inference
var x = 42        // int
var y = 3.14      // float64
var z = len("hi") // int
02

문자열 & 형식 지정

문자열 기본

Go 문자열은 불변 바이트 시퀀스입니다. len()은 문자 수가 아닌 바이트 수를 반환합니다 — Unicode에는 utf8.RuneCountInString()을 사용하세요. 문자열은 ==, <, >로 사전식으로 비교됩니다. 문자열을 range하면 rune(Unicode 코드 포인트)별로 반복하여 다중 바이트 문자를 올바르게 처리합니다.

go
package main

import (
    "fmt"
    "strings"
)

func main() {
    s := "Hello, World"

    // Length (bytes, not runes!)
    fmt.Println(len(s)) // 12

    // Concatenation
    s1 := "Hello" + " " + "World"
    s2 := fmt.Sprintf("%s = %d", "age", 30)

    // Comparison
    fmt.Println("abc" == "abc") // true
    fmt.Println("a" < "b")      // true (lexicographic)

    // Iteration (by rune)
    for i, r := range "Go语" {
        fmt.Printf("%d: %c\n", i, r)
    }
}

strings 패키지

strings 패키지는 일반적인 문자열 작업을 제공합니다. Contains/HasPrefix/HasSuffix는 부분 문자열을 검사합니다. Split은 구분자로 나누고; Join은 결합합니다. Replace는 카운트를 받습니다(모두는 -1). TrimSpace는 선행/후행 공백을 제거합니다. 모든 함수는 새 문자열을 반환합니다(문자열은 불변).

go
import (
    "fmt"
    "strings"
)

s := "Hello, World"

// Case
strings.ToUpper(s)         // "HELLO, WORLD"
strings.ToLower(s)         // "hello, world"
strings.Title("hello world") // "Hello World"

// Search
strings.Contains(s, "World")  // true
strings.HasPrefix(s, "Hello") // true
strings.HasSuffix(s, "World") // true
strings.Index(s, "World")     // 7 (-1 if not found)
strings.Count(s, "l")         // 3

// Split & Join
parts := strings.Split("a,b,c", ",")  // ["a", "b", "c"]
joined := strings.Join(parts, "-")    // "a-b-c"

// Replace
strings.Replace(s, "l", "L", 1)  // "HeLlo, World" (1 occurrence)
strings.ReplaceAll(s, "l", "L")  // "HeLLo, WorLd"
strings.TrimSpace("  hi  ")      // "hi"

strconv & 형식 지정

strconv는 문자열과 숫자를 변환합니다 — 항상 에러 반환을 확인하세요. fmt.Printf는 출력을 형식화합니다: %d(int), %f(float), %s(문자열), %t(bool), %T(타입), %x(16진수), %q(따옴표). 소수점 2자리에는 %.2f, 제로 패딩에는 %05d를 사용하세요. Sprintf는 출력 대신 문자열을 반환합니다.

go
import (
    "fmt"
    "strconv"
)

// String to number
n, err := strconv.Atoi("42")     // string -> int
f, err := strconv.ParseFloat("3.14", 64)

// Number to string
s1 := strconv.Itoa(42)            // int -> string
s2 := strconv.FormatFloat(3.14, 'f', 2, 64) // "3.14"

// Printf formatting
fmt.Printf("Int: %d\n", 42)
fmt.Printf("Float: %.2f\n", 3.14159)
fmt.Printf("String: %s\n", "hi")
fmt.Printf("Bool: %t\n", true)
fmt.Printf("Type: %T\n", 42)     // int
fmt.Printf("Hex: %x\n", 255)     // ff
fmt.Printf("Pad: %05d\n", 42)    // 00042
fmt.Printf("Quote: %q\n", "hi")  // "hi"

// Sprintf returns formatted string
result := fmt.Sprintf("Name: %s, Age: %d", "Alice", 30)

Raw 문자열 & 여러 줄

Raw 문자열(백틱)은 모든 것을 문자 그대로 보존합니다 — 이스케이프 시퀀스 없음, 여러 줄에 걸칠 수 있음. regex, SQL, HTML 템플릿에 사용하세요. 해석된 문자열(큰따옴표)은 \n, \t 등을 처리합니다. 루프에서 효율적인 문자열 연결을 위해 strings.Builder를 사용하세요(O(n²) 할당 방지).

go
// Raw string literal (backticks) - no escape processing
raw := `This is a
multiline string
with \n (literal backslash-n)`

// Interpreted string (double quotes) - processes escapes
interpreted := "Line1\nLine2\tTabbed"

// Raw strings are useful for:
// - Regex patterns
// - HTML/SQL templates
// - File paths on Windows
regex := `^d{4}-d{2}-d{2}$`

// String builder (efficient concatenation)
var sb strings.Builder
for i := 0; i < 1000; i++ {
    sb.WriteString("line\n")
}
result := sb.String()

Unicode & Rune

Go 문자열은 UTF-8로 인코딩된 바이트 시퀀스입니다. len()은 바이트를 제공하고; utf8.RuneCountInString()은 문자를 제공합니다. 문자열을 range하면 UTF-8이 자동으로 디코딩됩니다. 문자열을 수정하려면 []rune으로 변환하고, 변경하고, 다시 변환하세요. 이것은 국제화된 텍스트 처리에 필수적입니다.

go
import (
    "fmt"
    "unicode/utf8"
)

s := "Hello, 世界"

// Byte length vs rune length
fmt.Println(len(s))                    // 13 (bytes)
fmt.Println(utf8.RuneCountInString(s)) // 9 (runes/characters)

// Decode runes manually
for i := 0; i < len(s); {
    r, size := utf8.DecodeRuneInString(s[i:])
    fmt.Printf("%c ", r)
    i += size
}

// Range automatically decodes runes
for i, r := range s {
    fmt.Printf("%d:%c ", i, r)
}

// Rune slice (mutable)
runes := []rune("Hello")
runes[0] = 'J'
s2 := string(runes) // "Jello"
03

제어 흐름

If / Else

Go의 if는 조건 주변에 괄호가 필요 없지만, 중괄호는 필수입니다(한 줄 본문도). init 문(if x := f(); x > 0)은 if/else 블록으로 스코프됩니다 — 에러 검사에 일반적입니다. 이 패턴은 변수 스코프를 좁게 유지합니다.

go
// Basic if/else
score := 85
if score >= 90 {
    fmt.Println("A")
} else if score >= 80 {
    fmt.Println("B")
} else {
    fmt.Println("C")
}

// If with initialization statement
if n := computeValue(); n > 100 {
    fmt.Println("big:", n)
} else {
    fmt.Println("small:", n)
}
// n is not accessible here (scoped to if)

// No parentheses needed, but braces are required

For 루프

Go는 하나의 루프 키워드만 가집니다: 'for'. C 스타일, while 스타일(for cond), 무한(for) 및 이터레이션(for range)을 처리합니다. Range는 슬라이스, 맵, 문자열 및 채널과 작동합니다. 인덱스나 값을 건너뛰려면 _를 사용하세요. 맵 이터레이션 순서는 의도적으로 무작위입니다.

go
// C-style for
for i := 0; i < 5; i++ {
    fmt.Println(i)
}

// While-style (condition only)
n := 10
for n > 0 {
    n--
}

// Infinite loop
for {
    break
}

// Range (iterate slices, maps, strings)
nums := []int{10, 20, 30}
for index, value := range nums {
    fmt.Printf("%d: %d\n", index, value)
}

// Skip index or value with _
for _, value := range nums {
    fmt.Println(value)
}

// Range over map (unordered!)
m := map[string]int{"a": 1, "b": 2}
for key, val := range m {
    fmt.Println(key, val)
}

Switch

Go의 switch는 기본적으로 떨어지지 않습니다(C/Java와 다름) — 각 case는 별도의 분기입니다. 강제하려면 fallthrough를 사용하세요. case당 여러 값은 쉼표를 사용합니다. 표현식이 없는 switch는 더 깔끔한 if/else 체인으로 작동합니다. init 문이 있는 switch는 변수를 switch로 스코프합니다.

go
// Basic switch
day := 3
switch day {
case 1:
    fmt.Println("Mon")
case 2, 3, 4, 5:
    fmt.Println("Weekday")
case 6, 7:
    fmt.Println("Weekend")
default:
    fmt.Println("Invalid")
}

// Switch with no expression (like if/else chain)
switch {
case score >= 90:
    grade = "A"
case score >= 80:
    grade = "B"
default:
    grade = "C"
}

// Switch with init
switch os := runtime.GOOS; os {
case "linux":
    fmt.Println("Linux")
case "darwin":
    fmt.Println("macOS")
}

// Fallthrough (rare, goes to next case unconditionally)
switch 1 {
case 1:
    fmt.Println("one")
    fallthrough
case 2:
    fmt.Println("two") // executes even though value is 1
}

Defer

defer는 둘러싸는 함수가 반환될 때 실행될 함수 호출을 예약합니다 — LIFO(후입선출). 정리(파일 닫기, 락 해제, 연결 닫기)에 사용하세요. 지연된 호출의 인수는 즉시 평가되지만, 호출은 반환 시 실행됩니다. 함수가 패닉되어도 defer는 실행됩니다.

go
// Defer runs when function returns (LIFO order)
func main() {
    defer fmt.Println("third")
    defer fmt.Println("second")
    fmt.Println("first")
}
// Output: first, second, third

// Common: resource cleanup
func readFile(path string) error {
    f, err := os.Open(path)
    if err != nil {
        return err
    }
    defer f.Close() // runs when readFile returns

    // ... use f ...
    return nil
}

// Defer evaluates arguments immediately
i := 1
defer fmt.Println(i) // prints 1 (not 2)
i = 2

Goto, Break, Continue

break는 가장 안쪽 루프를 종료하고; continue는 다음 반복으로 건너뜁니다. 레이블(break outer)은 중첩 루프를 종료합니다. goto는 존재하지만 거의 사용되지 않습니다 — 구조화된 제어 흐름을 선호하세요. 레이블은 외부 루프의 다음 반복으로 건너뛰기 위해 continue와 함께 사용할 수도 있습니다.

go
// Break and continue
for i := 0; i < 10; i++ {
    if i%2 == 0 {
        continue // skip even numbers
    }
    if i > 7 {
        break // exit loop
    }
    fmt.Println(i)
}

// Break with label (for nested loops)
outer:
for i := 0; i < 3; i++ {
    for j := 0; j < 3; j++ {
        if i == 1 && j == 1 {
            break outer // exits both loops
        }
    }
}

// Goto (rarely used, avoid)
i := 0
loop:
    if i < 5 {
        fmt.Println(i)
        i++
        goto loop
    }

Select(채널 작업)

select는 채널용 switch와 같습니다 — 여러 채널 작업을 대기하고 준비된 첫 번째 것을 선택합니다. default case는 비차단으로 만듭니다. 이벤트 기반 패턴을 위해 루프에서 select를 사용하세요. time.After()는 타임아웃 채널을 만듭니다. 여러 case가 준비되면 select는 무작위로 선택하여 기아를 방지합니다.

go
// Select waits on multiple channel operations
select {
case msg := <-messages:
    fmt.Println("received:", msg)
case <-timeout:
    fmt.Println("timed out")
default:
    fmt.Println("no activity") // non-blocking
}

// Select with send
ch1 := make(chan int)
ch2 := make(chan int)
go func() { ch1 <- 1 }()
go func() { ch2 <- 2 }()

select {
case v := <-ch1:
    fmt.Println("ch1:", v)
case v := <-ch2:
    fmt.Println("ch2:", v)
}

// Select in a loop (event loop pattern)
for {
    select {
    case msg := <-ch:
        fmt.Println(msg)
    case <-time.After(5 * time.Second):
        return // timeout after 5s of inactivity
    }
}
04

함수

정의 & 다중 반환

Go 함수는 여러 값을 반환할 수 있습니다 — 정식 패턴은 (result, error)입니다. 명명된 반환은 제로 값으로 초기화된 선언된 변수로 작동합니다; '네이키드' 반환은 그것들을 사용합니다. 명명된 반환은 복잡한 함수의 가독성을 높이지만 남용하면 혼란스러울 수 있습니다. 항상 에러를 즉시 확인하세요.

go
// Function with multiple return values
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, fmt.Errorf("divide by zero")
    }
    return a / b, nil
}

// Named return values
func split(sum int) (x, y int) {
    x = sum * 4 / 9
    y = sum - x
    return // "naked" return (uses named values)
}

// Usage
result, err := divide(10, 2)
if err != nil {
    log.Fatal(err)
}
fmt.Println(result) // 5

x, y := split(100)
fmt.Println(x, y)

가변 인수 & 클로저

가변 인수 함수(...T)는 임의의 수의 인수를 받아 슬라이스로 받습니다. ...로 슬라이스를 전개하세요. 클로저는 둘러싸는 스코프에서 참조로 변수를 캡처합니다 — counter() 함수는 'count'를 기억하는 클로저를 반환합니다. 클로저는 콜백, 이터레이터 및 상태 저장 함수에 유용합니다.

go
// Variadic function (variable arguments)
func sum(nums ...int) int {
    total := 0
    for _, n := range nums {
        total += n
    }
    return total
}
fmt.Println(sum(1, 2, 3))       // 6
fmt.Println(sum(1, 2, 3, 4, 5)) // 15

// Spread a slice
nums := []int{1, 2, 3}
fmt.Println(sum(nums...)) // 6

// Closure (function that captures variables)
func counter() func() int {
    count := 0
    return func() int {
        count++
        return count
    }
}

next := counter()
fmt.Println(next()) // 1
fmt.Println(next()) // 2
fmt.Println(next()) // 3

값으로서의 함수

Go의 함수는 일급 값입니다 — 변수에 할당, 인수로 전달 및 데이터 구조에 저장할 수 있습니다. 'type Name func(params) returns'로 함수 타입을 정의하세요. 함수의 맵은 디스패치 테이블, 명령 핸들러 및 전략 패턴에 유용합니다.

go
// Functions are first-class values
func apply(f func(int) int, x int) int {
    return f(x)
}

func double(x int) int { return x * 2 }
func square(x int) int { return x * x }

fmt.Println(apply(double, 5)) // 10
fmt.Println(apply(square, 5)) // 25

// Function type
type MathFunc func(int) int

var fn MathFunc = func(x int) int { return x + 1 }
fmt.Println(fn(10)) // 11

// Map of functions
ops := map[string]func(int, int) int{
    "add": func(a, b int) int { return a + b },
    "sub": func(a, b int) int { return a - b },
    "mul": func(a, b int) int { return a * b },
}
fmt.Println(ops["add"](3, 4)) // 7

init() & 익명 함수

init() 함수는 main() 전에 선언된 순서대로 자동으로 실행됩니다. 설정(설정 로딩, 검증, 등록)에 사용하세요. 파일당 여러 init() 함수가 허용됩니다. 익명 함수는 즉시 호출(IIFE)되거나 변수에 할당될 수 있습니다. 고루틴에도 사용됩니다.

go
// init() runs before main(), once per package
// Multiple init() functions allowed, run in order declared
package main

import "fmt"

var config string

func init() {
    // Setup code: config, connections, validation
    config = "production"
    fmt.Println("init 1")
}

func init() {
    fmt.Println("init 2, config:", config)
}

func main() {
    fmt.Println("main")
}
// Output: init 1, init 2, main

// Anonymous function (IIFE)
result := func(x int) int {
    return x * 2
}(5) // immediately invoked, result = 10

// Goroutine with anonymous function
go func(msg string) {
    fmt.Println(msg)
}("async")

제네릭(Go 1.18+)

Go 1.18+은 타입 매개변수 [T any]로 제네릭을 지원합니다. 타입 제약(인터페이스)은 허용되는 타입을 제한합니다 — 제약 없이는 'any'를, == / != 연산자에는 'comparable'을 사용하세요. Union 제약(int | float64)은 특정 타입을 허용합니다. 제네릭은 코드 중복 없이 타입 안전 재사용 가능한 데이터 구조와 알고리즘을 가능하게 합니다.

go
// Generic function with type parameter
func Map[T, U any](slice []T, f func(T) U) []U {
    result := make([]U, len(slice))
    for i, v := range slice {
        result[i] = f(v)
    }
    return result
}

nums := []int{1, 2, 3}
doubled := Map(nums, func(n int) int { return n * 2 })
// [2, 4, 6]

strs := Map(nums, func(n int) string {
    return fmt.Sprintf("num%d", n)
})
// ["num1", "num2", "num3"]

// Type constraints
type Number interface {
    int | int64 | float64
}

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

// comparable constraint (for maps/slices keys)
func Contains[T comparable](slice []T, target T) bool {
    for _, v := range slice {
        if v == target {
            return true
        }
    }
    return false
}

메서드

메서드는 리시버 인수가 있는 함수입니다. 값 리시버(r Rectangle)는 사본으로 작동합니다 — 원본을 수정할 수 없습니다. 포인터 리시버(r *Rectangle)는 수정할 수 있고 큰 struct 복사를 피합니다. 일관성이 중요합니다: 하나의 메서드가 포인터 리시버를 사용하면 모두 그래야 합니다. 메서드는 동일 패키지의 모든 타입에 정의할 수 있습니다.

go
type Rectangle struct {
    Width, Height float64
}

// Method with value receiver
func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

// Method with pointer receiver (can modify)
func (r *Rectangle) Scale(factor float64) {
    r.Width *= factor
    r.Height *= factor
}

// Method on non-struct types
type MyString string

func (s MyString) Shout() string {
    return strings.ToUpper(string(s)) + "!"
}

// Usage
r := Rectangle{Width: 10, Height: 5}
fmt.Println(r.Area()) // 50
r.Scale(2)
fmt.Println(r.Width)  // 20

s := MyString("hello")
fmt.Println(s.Shout()) // HELLO!
05

데이터 구조

배열 & 슬라이스

배열은 고정 길이를 가집니다; 슬라이스는 동적입니다(배열 기반). append()는 요소를 추가하고 필요에 따라 용량을 늘립니다. make([]T, len, cap)는 효율성을 위해 미리 할당합니다. 슬라이싱은 뷰를 만듭니다(기본 배열 공유) — 독립적인 데이터에는 copy()를 사용하세요. 최적화할 때 항상 len/cap을 확인하세요.

go
// Array (fixed length, rarely used directly)
var arr [3]int = [3]int{1, 2, 3}
arr2 := [...]int{4, 5, 6} // size inferred

// Slice (dynamic array, most common)
nums := []int{1, 2, 3}
nums = append(nums, 4)        // [1, 2, 3, 4]
nums = append(nums, 5, 6, 7) // [1, 2, 3, 4, 5, 6, 7]

// Make a slice with capacity
s := make([]int, 3, 10) // len=3, cap=10

// Slicing
sub := nums[1:4]  // [2, 3, 4]
first := nums[:2] // [1, 2]
last := nums[3:]  // [4, 5, 6, 7]

// Copy
dst := make([]int, len(nums))
copy(dst, nums)

// Length and capacity
fmt.Println(len(nums), cap(nums))

맵은 Go의 해시 테이블입니다 — 순서 없는 키/값 쌍. comma-ok 패턴(val, ok := m[key])은 키가 존재하는지 검사합니다. delete()는 키를 제거합니다. 맵 이터레이션 순서는 의도적으로 무작위입니다. 맵은 참조 타입입니다 — 함수에 전달하면 기본 데이터가 공유됩니다. nil 맵은 쓸 수 없습니다(make() 사용).

go
// Create a map
m := map[string]int{
    "Alice": 30,
    "Bob":   25,
}

// Using make
ages := make(map[string]int)
ages["Charlie"] = 35

// Access
fmt.Println(m["Alice"]) // 30

// Check existence (comma-ok pattern)
age, ok := m["David"]
if !ok {
    fmt.Println("not found")
}

// Delete
delete(m, "Bob")

// Iterate (random order)
for name, age := range m {
    fmt.Printf("%s: %d\n", name, age)
}

// Length
fmt.Println(len(m))

// Nested maps
matrix := map[string]map[string]int{}
matrix["row1"] = map[string]int{"col1": 1}

구조체

구조체는 관련 필드를 그룹화합니다. 명확성을 위해 명명된 초기화(Person{Name: ...})를 사용하세요. new()는 제로 값으로 포인터를 반환합니다. 익명 구조체는 일회성 데이터 모양에 유용합니다. 구조체 임베딩(필드 이름 없음)은 임베드된 구조체의 필드와 메서드를 승격합니다 — Go의 상속 대안.

go
// Define a struct
type Person struct {
    Name string
    Age  int
    Address string // field names are exported (capitalized)
}

// Create instances
p1 := Person{Name: "Alice", Age: 30}
p2 := Person{"Bob", 25, "NYC"} // positional (not recommended)
p3 := new(Person)              // returns *Person, zero values
p3.Name = "Charlie"

// Anonymous struct (one-off)
config := struct {
    Port int
    Host string
}{
    Port: 8080,
    Host: "localhost",
}

// Struct embedding (composition)
type Employee struct {
    Person          // embedded (promotes fields)
    Salary   float64
}

emp := Employee{
    Person: Person{Name: "Dave", Age: 40},
    Salary: 50000,
}
fmt.Println(emp.Name) // "Dave" (promoted from Person)

포인터

Go 포인터(*T)는 메모리 주소를 보유합니다. &는 주소를 가져오고, *는 역참조합니다. C와 달리 Go에는 포인터 연산이 없습니다(더 안전). 구조체 포인터는 약식을 허용합니다(u.Name 대신 (*u).Name). nil 포인터는 역참조 시 패닉을 일으킵니다. Go에는 가비지 컬렉션이 있습니다 — 수동 free()가 필요 없습니다.

go
// Pointer basics
x := 42
p := &x          // p is *int, points to x
fmt.Println(*p)  // 42 (dereference)
*p = 100         // modify x through pointer
fmt.Println(x)   // 100

// new() allocates and returns pointer
p2 := new(int)   // *int, value 0
*p2 = 42

// Pointers to structs
type User struct{ Name string }
u := &User{Name: "Alice"} // *User
u.Name = "Bob"            // (*u).Name shorthand
fmt.Println(u.Name)       // Bob

// nil pointer
var p3 *int // nil
// *p3 // panic: nil pointer dereference

// No pointer arithmetic (unlike C)
// p++ is illegal

슬라이스 작업

Go에는 내장 filter/map/reduce가 없습니다 — 직접 작성하거나 slices 패키지(Go 1.21+)를 사용하세요. Insert/remove는 append+copy로 요소를 이동해야 합니다. 슬라이스 별칭에 주의하세요: s[:i]와 s[i+1:]는 기본 배열을 공유합니다. 2D 슬라이스의 경우 각 행을 별도로 할당하세요.

go
// Filter
func filter(nums []int, pred func(int) bool) []int {
    result := []int{}
    for _, n := range nums {
        if pred(n) {
            result = append(result, n)
        }
    }
    return result
}

evens := filter([]int{1,2,3,4,5}, func(n int) bool {
    return n%2 == 0
}) // [2, 4]

// Insert at index
func insert(s []int, i, v int) []int {
    s = append(s, 0)
    copy(s[i+1:], s[i:])
    s[i] = v
    return s
}

// Remove at index
func remove(s []int, i int) []int {
    return append(s[:i], s[i+1:]...)
}

// 2D slice
matrix := make([][]int, 3)
for i := range matrix {
    matrix[i] = make([]int, 4)
}

정렬 & 검색

sort.Ints/Strings/Float64s는 in-place로 정렬합니다. sort.Slice는 비교자로 사용자 정의 타입을 처리합니다. sort.Search*는 정렬된 슬라이스에서 이진 검색을 수행합니다. Go 1.21+은 제네릭 정렬 함수가 있는 slices 패키지를 추가합니다. 모든 정렬은 in-place입니다 — 원래 순서가 필요하면 먼저 사본을 만드세요.

go
import "sort"

// Sort a slice
nums := []int{3, 1, 4, 1, 5, 9, 2, 6}
sort.Ints(nums) // [1 1 2 3 4 5 6 9]
fmt.Println(nums)

// Sort strings
strs := []string{"banana", "apple", "cherry"}
sort.Strings(strs) // [apple banana cherry]

// Sort with custom comparator
type Person struct {
    Name string
    Age  int
}

people := []Person{
    {"Alice", 30},
    {"Bob", 25},
    {"Charlie", 35},
}

sort.Slice(people, func(i, j int) bool {
    return people[i].Age < people[j].Age // by age ascending
})

// Binary search (sorted slice)
idx := sort.SearchInts(nums, 4) // index of 4

// Sort package (Go 1.21+): slices.Sort
import "slices"
slices.Sort(people) // needs Less method or cmp func
06

메서드 & 인터페이스

인터페이스 정의

인터페이스는 메서드 시그니처를 정의합니다. Go는 구조적 타이핑을 사용합니다 — 타입이 모든 필수 메서드를 가지면 자동으로 인터페이스를 구현합니다(명시적 'implements' 선언 없음). 이는 분리된 설계를 가능하게 합니다: 인터페이스를 구현하는 곳이 아닌 사용하는 곳에서 정의하세요.

go
// Interface: a set of method signatures
type Shape interface {
    Area() float64
    Perimeter() float64
}

// Implement implicitly (no 'implements' keyword)
type Circle struct {
    Radius float64
}

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

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

// Use the interface
func describe(s Shape) {
    fmt.Printf("Area: %.2f, Perimeter: %.2f\n",
        s.Area(), s.Perimeter())
}

c := Circle{Radius: 5}
describe(c) // works! Circle implements Shape

빈 인터페이스 & 타입 어설션

interface{}(또는 Go 1.18+의 'any')는 모든 값을 보유합니다. 타입 어설션(v.(T))은 구체적 타입을 추출합니다 — 잘못된 타입이면 패닉이 발생하므로 comma-ok 패턴을 사용하세요. 타입 switch(switch v.(type))는 여러 타입을 깔끔하게 처리합니다. 빈 인터페이스는 일반 컨테이너에 유용하지만 타입 안전성을 잃습니다 — 제네릭을 선호하세요.

go
// Empty interface (any type)
// Go 1.18+: 'any' is an alias for interface{}
func printAny(v any) {
    fmt.Println(v)
}

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

// Type assertion
var i any = "hello"
s := i.(string)    // panics if not string
fmt.Println(s)

// Safe type assertion (comma-ok)
s, ok := i.(string)
if ok {
    fmt.Println("string:", s)
}

// Type switch
func describe(v any) {
    switch x := v.(type) {
    case int:
        fmt.Printf("int: %d\n", x)
    case string:
        fmt.Printf("string: %s\n", x)
    case []int:
        fmt.Printf("int slice: %v\n", x)
    default:
        fmt.Printf("unknown type: %T\n", x)
    }
}

인터페이스 컴포지션

인터페이스는 다른 인터페이스를 임베드할 수 있습니다(컴포지션). io.Reader와 io.Writer는 Go의 가장 중요한 인터페이스입니다 — 파일, 네트워크 연결, 버퍼 등에 의해 구현됩니다. 이는 강력한 추상화를 가능하게 합니다: io.Reader를 받는 함수는 모든 읽기 가능한 소스와 작동합니다. 작고 집중된 인터페이스(1-3 메서드)가 관용적입니다.

go
// Compose interfaces
type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

// ReadWriter combines Reader and Writer
type ReadWriter interface {
    Reader
    Writer
}

// io.Reader and io.Writer are built-in interfaces
// Many types implement them: *os.File, *bytes.Buffer, net.Conn

func copyData(r io.Reader, w io.Writer) error {
    buf := make([]byte, 1024)
    for {
        n, err := r.Read(buf)
        if n > 0 {
            if _, err := w.Write(buf[:n]); err != nil {
                return err
            }
        }
        if err == io.EOF {
            break
        }
        if err != nil {
            return err
        }
    }
    return nil
}

포인터 vs 값 리시버

포인터 리시버는 구조체를 수정하고 큰 값의 복사를 피할 수 있습니다. 값 리시버는 안전하고(수정 불가) 값과 포인터 모두에서 메서드를 허용합니다. 하나의 메서드가 포인터 리시버를 가지면 모두 그래야 합니다(일관성). 포인터 리시버 메서드는 T와 *T 모두에 대해 인터페이스를 만족합니다; 값 리시버 메서드는 T에 대해서만.

go
type Counter struct {
    count int
}

// Value receiver: works on a copy, can't modify
func (c Counter) Get() int {
    return c.count
}

// Pointer receiver: can modify the original
func (c *Counter) Increment() {
    c.count++
}

c := Counter{}
c.Increment() // Go auto-takes address (&c).Increment()
c.Increment()
fmt.Println(c.Get()) // 2

// Interface implementation note:
// *Counter implements both Get() and Increment()
// Counter (value) implements only Get()
// So *Counter satisfies an interface requiring both

Stringer 인터페이스

Stringer 인터페이스(String() string)는 Print/Printf에서 타입이 나타나는 방식을 제어합니다 — Java/JS의 toString()과 같습니다. %v는 String()을 사용하고; %+v는 필드 이름을 표시하고; %#v는 Go 구문을 표시합니다. error 인터페이스도 동일하게 작동합니다: Error() string을 구현하여 모든 타입을 error로 만드세요. 이들은 Go의 가장 일반적인 내장 인터페이스입니다.

go
// Stringer interface (like toString() in other languages)
type Stringer interface {
    String() string
}

type Person struct {
    Name string
    Age  int
}

// Implement Stringer
func (p Person) String() string {
    return fmt.Sprintf("%s (%d years)", p.Name, p.Age)
}

p := Person{Name: "Alice", Age: 30}
fmt.Println(p)       // Alice (30 years) — uses String()
fmt.Printf("%v\n", p) // Alice (30 years)
fmt.Printf("%+v\n", p) // Name:Alice Age:30
fmt.Printf("%#v\n", p) // main.Person{Name:"Alice", Age:30}

// Error interface is similar:
// type error interface { Error() string }
// Implement Error() to make a type usable as an error

타입 임베딩 & 컴포지션

Go는 상속 대신 컴포지션을 사용합니다. 구조체를 임베드하면 그 필드와 메서드가 외부 구조체로 승격됩니다. 외부 타입에 동일한 이름의 메서드를 정의하여 승격된 메서드를 재정의할 수 있습니다. 인터페이스를 임베드하면 장식/위임이 가능합니다 — 외부 타입이 인터페이스를 만족하고 호출을 전달할 수 있습니다.

go
// Embed a struct (composition over inheritance)
type Animal struct {
    Name string
}

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

type Dog struct {
    Animal // embedded — promotes Name and Speak()
    Breed  string
}

d := Dog{
    Animal: Animal{Name: "Rex"},
    Breed:  "Labrador",
}

fmt.Println(d.Name)      // "Rex" (promoted)
fmt.Println(d.Speak())   // "Rex makes a sound" (promoted)
fmt.Println(d.Animal.Name) // explicit access also works

// Override a promoted method
func (d Dog) Speak() string {
    return d.Name + " barks!"
}
fmt.Println(d.Speak()) // "Rex barks!"

// Embed an interface
type Logger struct {
    io.Writer // embed interface
}
// Logger now has Write() method, delegating to the embedded Writer
07

에러 처리

에러 기본

Go는 예외가 아닌 값으로 에러를 처리합니다. 함수는 (result, error)를 반환합니다 — 항상 err != nil을 즉시 확인하세요. errors.New()는 단순한 에러를 만들고; fmt.Errorf()는 형식화를 추가합니다. 센티넬 에러(var ErrX = errors.New())는 ==로 비교를 가능하게 합니다. 에러를 절대 무시하지 마세요(_는 의도적일 때만 사용).

go
// Functions return errors as last return value
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("cannot divide by zero")
    }
    return a / b, nil
}

// Always check errors immediately
result, err := divide(10, 0)
if err != nil {
    log.Fatal(err) // or handle gracefully
}
fmt.Println(result)

// fmt.Errorf for formatted errors
func validate(age int) error {
    if age < 0 {
        return fmt.Errorf("invalid age: %d (must be positive)", age)
    }
    return nil
}

// Sentinel errors
var ErrNotFound = errors.New("not found")
if err := lookup("key"); err == ErrNotFound {
    // handle not found
}

사용자 정의 에러 타입

사용자 정의 에러 타입은 error 인터페이스(Error() string)를 구현합니다. 단순한 메시지 이상의 구조화된 데이터(필드, 코드, 컨텍스트)를 전달합니다. 타입 어설션(*ValidationError)으로 데이터에 접근하세요. 이것은 도메인별 에러 처리에 필수적입니다 — 예: HTTP 상태 코드, 검증 세부 정보, 재시도 로직.

go
// Custom error type (implements error interface)
type ValidationError struct {
    Field   string
    Message string
}

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

// Use the custom error
func validateUser(name string) error {
    if name == "" {
        return &ValidationError{
            Field:   "name",
            Message: "cannot be empty",
        }
    }
    return nil
}

// Type-assert to access fields
err := validateUser("")
if ve, ok := err.(*ValidationError); ok {
    fmt.Println("Field:", ve.Field)   // "name"
    fmt.Println("Message:", ve.Message) // "cannot be empty"
}

errors.Is & errors.As(Go 1.13+)

Go 1.13+는 %w(fmt.Errorf)로 에러 래핑을 추가했습니다. errors.Is()는 에러가 센티넬과 일치하는지 검사합니다(체인 언래핑). errors.As()는 체인에서 특정 에러 타입을 추출합니다. 이를 통해 계층화된 에러 처리가 가능합니다: 저수준 에러가 컨텍스트와 함께 래핑되고 고수준에서 검사됩니다. 체인을 보존하려면 항상 %v가 아닌 %w로 래핑하세요.

go
import "errors"

// Wrapped errors with %w
var ErrNotFound = errors.New("not found")

func getUser(id int) error {
    if id == 0 {
        return fmt.Errorf("getUser(%d): %w", id, ErrNotFound)
    }
    return nil
}

// errors.Is: check if error matches (unwraps the chain)
err := getUser(0)
if errors.Is(err, ErrNotFound) {
    fmt.Println("user not found")
}

// errors.As: extract a specific error type
var ve *ValidationError
if errors.As(err, &ve) {
    fmt.Println("field:", ve.Field)
}

// errors.Unwrap: get the wrapped error
inner := errors.Unwrap(err)

Panic & Recover

panic()은 복구 불가능한 에러(버그, 불변 조건 위반)를 위한 것입니다 — 정상적인 에러 처리가 아닙니다. recover()는 패닉을 잡지만 지연된 함수에서만 작동합니다. panic/recover를 사용하세요: 프로그래밍 에러(인덱스 범위 초과), 패키지 초기화 실패, 고루틴이 프로그램을 충돌시키는 것으로부터 보호. 예상되는 실패에는 에러 반환을 선호하세요.

go
// Panic: unrecoverable error (like throw)
func mustParse(s string) int {
    n, err := strconv.Atoi(s)
    if err != nil {
        panic(fmt.Sprintf("invalid number: %s", s))
    }
    return n
}

// Recover: catch a panic (only in deferred functions)
func safeCall() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("recovered:", r)
        }
    }()
    panic("something went wrong")
}

// Practical: safe goroutine
func safeGo(fn func()) {
    go func() {
        defer func() {
            if r := recover(); r != nil {
                log.Println("goroutine panic:", r)
            }
        }()
        fn()
    }()
}

에러 래핑 패턴

%w와 함께 fmt.Errorf를 사용하여 각 계층에서 컨텍스트와 함께 에러를 래핑하세요. 이것은 에러 체인을 만듭니다: 최상위는 전체 경로를 봅니다(getUserProfile → fetchUser → sql error). 컨텍스트(함수 이름, 매개변수)는 디버깅에 도움입니다. errors.Is/As로 체인에서 특정 에러를 검사하세요. 동일한 컨텍스트로 동일한 에러를 여러 번 래핑하지 마세요.

go
// Wrap errors with context as they propagate up
func fetchUser(id int) (*User, error) {
    row := db.QueryRow("SELECT ... WHERE id = ?", id)
    var u User
    if err := row.Scan(&u.Name, &u.Age); err != nil {
        return nil, fmt.Errorf("fetchUser(%d): %w", id, err)
    }
    return &u, nil
}

func getUserProfile(id int) (*Profile, error) {
    user, err := fetchUser(id)
    if err != nil {
        return nil, fmt.Errorf("getUserProfile(%d): %w", id, err)
    }
    // ...
}

// At the top level, log the full chain
profile, err := getUserProfile(42)
if err != nil {
    log.Printf("error: %v", err)
    // Output: getUserProfile(42): fetchUser(42): sql: no rows
}

// golang.org/x/xerrors or pkg/errors for stack traces

Defer, Panic, Recover 함께

defer(정리), panic(치명적 에러), recover(패닉 잡기)를 결합하여 강건한 리소스 관리를 하세요. defer는 패닉 중에도 LIFO 순서로 실행됩니다. 명명된 반환 값(err error)은 지연된 recover 내에서 설정할 수 있습니다. 이 패턴은 코드가 패닉되어도 파일/연결이 닫히고 트랜잭션이 롤백되도록 보장합니다.

go
// Complete pattern: cleanup + panic recovery
func processFile(path string) (err error) {
    file, e := os.Open(path)
    if e != nil {
        return e
    }
    defer func() {
        // Close file regardless of panic
        file.Close()

        // Recover from panic and convert to error
        if r := recover(); r != nil {
            err = fmt.Errorf("panic in processFile: %v", r)
        }
    }()

    // Do work that might panic
    data := parseFile(file) // might panic
    return saveData(data)
}

// Defer for resource cleanup (always runs)
func withDatabase(db *sql.DB, fn func(*sql.DB) error) error {
    tx, err := db.Begin()
    if err != nil {
        return err
    }
    defer func() {
        if err != nil {
            tx.Rollback()
        } else {
            tx.Commit()
        }
    }()
    return fn(tx)
}
08

동시성

고루틴

고루틴은 Go의 경량 스레드입니다 — 'go'로 시작합니다. 저렴합니다(~2KB 스택) 그리고 Go의 런타임 스케줄러(M:N 스케줄링)에 의해 관리됩니다. main 함수는 고루틴을 기다리지 않습니다 — 동기화를 위해 sync.WaitGroup이나 채널을 사용하세요. 프로덕션에서 동기화를 위해 time.Sleep을 절대 사용하지 마세요(WaitGroup 사용).

go
package main

import (
    "fmt"
    "time"
)

func sayHello(name string) {
    for i := 0; i < 3; i++ {
        fmt.Println(name, i)
        time.Sleep(100 * time.Millisecond)
    }
}

func main() {
    // Launch goroutine with 'go' keyword
    go sayHello("Alice")
    go sayHello("Bob")

    // Anonymous goroutine
    go func() {
        fmt.Println("anonymous goroutine")
    }()

    // Wait for goroutines (simple approach)
    time.Sleep(1 * time.Second)
    fmt.Println("done")
}

// Goroutines are lightweight (~2KB stack, grows as needed)
// Millions of goroutines can run concurrently

채널

채널은 고루틴 통신을 위한 타입화된 도관입니다. 버퍼 없음(make(chan T))은 송신자와 수신자가 모두 준비될 때까지 차단합니다(동기식). 버퍼 있음(make(chan T, n))은 가득 찰 때만 차단합니다(비동기식). 수신자가 아닌 송신자가 채널을 닫아야 합니다. 채널이 닫힐 때까지 range하세요. 채널은 '통신으로 메모리 공유'를 가능하게 합니다.

go
// Unbuffered channel (synchronous)
ch := make(chan string)

// Send and receive
go func() {
    ch <- "hello" // blocks until received
}()
msg := <-ch // blocks until sent
fmt.Println(msg)

// Buffered channel (asynchronous)
buf := make(chan int, 3)
buf <- 1 // doesn't block (buffer has space)
buf <- 2
buf <- 3
// buf <- 4 // would block (buffer full)
fmt.Println(<-buf) // 1

// Close a channel (sender closes, never receiver)
close(buf)

// Range over channel (until closed)
for v := range buf {
    fmt.Println(v)
}

// Check if closed
v, ok := <-buf
if !ok {
    fmt.Println("channel closed")
}

Select 문

select는 고루틴이 여러 채널 작업을 대기하게 합니다 — 준비된 첫 번째 것을 선택합니다(여러 개면 무작위). default case는 비차단으로 만듭니다. time.After()는 타임아웃 채널을 만듭니다. select는 동시성 Go의 핵심입니다: 이벤트 루프, fan-in/fan-out, 타임아웃. 교착 상태를 피하기 위해 항상 타임아웃을 포함하세요.

go
// Select: multiplex channel operations
ch1 := make(chan string)
ch2 := make(chan string)

go func() {
    time.Sleep(1 * time.Second)
    ch1 <- "one"
}()
go func() {
    time.Sleep(2 * time.Second)
    ch2 <- "two"
}()

// Wait for first to arrive
for i := 0; i < 2; i++ {
    select {
    case msg1 := <-ch1:
        fmt.Println("received:", msg1)
    case msg2 := <-ch2:
        fmt.Println("received:", msg2)
    }
}

// Timeout with select
select {
case result := <-slowOperation():
    fmt.Println(result)
case <-time.After(3 * time.Second):
    fmt.Println("timeout!")
}

// Non-blocking receive (default case)
select {
case msg := <-ch:
    fmt.Println(msg)
default:
    fmt.Println("no message") // runs if no data
}

sync.WaitGroup

sync.WaitGroup은 고루틴 그룹이 완료되기를 기다립니다. Add(n)은 카운터를 증가시키고, Done()은 감소시키며(defer 사용), Wait()은 0이 될 때까지 차단합니다. 클로저 캡처 버그를 피하기 위해 항상 루프 변수를 매개변수로 고루틴에 전달하세요(Go 1.22에서 수정되었지만 여전히 권장). WaitGroup은 fire-and-forget 동시성을 위해 채널보다 단순합니다.

go
import "sync"

func main() {
    var wg sync.WaitGroup

    // Launch 5 goroutines
    for i := 0; i < 5; i++ {
        wg.Add(1) // increment counter
        go func(id int) {
            defer wg.Done() // decrement when done
            fmt.Printf("Worker %d started\n", id)
            time.Sleep(time.Duration(id) * 100 * time.Millisecond)
            fmt.Printf("Worker %d done\n", id)
        }(i)
    }

    wg.Wait() // block until all Done() called
    fmt.Println("all workers finished")
}

// Common mistake: passing loop variable to goroutine
// Always pass as parameter: go func(i int) { ... }(i)
// (Go 1.22+ fixes loop variable scoping, but still good practice)

Mutex & Sync

sync.Mutex는 동시 접근으로부터 공유 상태를 보호합니다 — defer와 함께 Lock/Unlock. RWMutex는 여러 읽기 또는 하나의 쓰기를 허용합니다(읽기 많은 작업에 더 좋음). sync.Once는 초기화가 정확히 한 번 발생하도록 보장합니다(싱글톤 패턴). 통신에는 채널을, 공유 상태 보호에는 뮤텍스를 선호하세요. '통신으로 메모리 공유; 메모리 공유로 통신하지 마세요.'

go
import "sync"

// Mutex: mutual exclusion lock
type SafeCounter struct {
    mu    sync.Mutex
    count int
}

func (c *SafeCounter) Increment() {
    c.mu.Lock()
    defer c.mu.Unlock() // always unlock with defer
    c.count++
}

func (c *SafeCounter) Value() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.count
}

// RWMutex: multiple readers, one writer
type Cache struct {
    mu   sync.RWMutex
    data map[string]string
}

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

func (c *Cache) Set(key, val string) {
    c.mu.Lock()          // write lock (exclusive)
    defer c.mu.Unlock()
    c.data[key] = val
}

// sync.Once: run exactly once
var (
    once sync.Once
    instance *Database
)

func GetDB() *Database {
    once.Do(func() {
        instance = &Database{}
    })
    return instance
}

동시성 패턴

워커 풀: 고정된 수의 고루틴이 채널에서 작업을 처리하고 결과를 다른 채널로 보냅니다. 이것은 동시성을 제한하고 리소스 고갈을 방지합니다. Fan-out/fan-in: 작업을 고루틴에 분산시킨 다음 결과를 병합. 핵심 통찰: range 루프가 종료되도록 더 이상 데이터가 전송되지 않을 때 채널을 닫으세요. 이 패턴들은 동시성 Go의 기초입니다.

go
// Worker pool
func worker(id int, jobs <-chan int, results chan<- int) {
    for j := range jobs {
        time.Sleep(time.Second) // simulate work
        results <- j * 2
    }
}

func main() {
    jobs := make(chan int, 100)
    results := make(chan int, 100)

    // Start 3 workers
    for w := 1; w <= 3; w++ {
        go worker(w, jobs, results)
    }

    // Send 5 jobs
    for j := 1; j <= 5; j++ {
        jobs <- j
    }
    close(jobs)

    // Collect results
    for a := 1; a <= 5; a++ {
        fmt.Println(<-results)
    }
}

// Fan-out/Fan-in
func fanOut(input <-chan int, workers int) <-chan int {
    output := make(chan int)
    var wg sync.WaitGroup
    wg.Add(workers)
    for i := 0; i < workers; i++ {
        go func() {
            defer wg.Done()
            for v := range input {
                output <- v * 2
            }
        }()
    }
    go func() { wg.Wait(); close(output) }()
    return output
}
09

파일 I/O & OS

파일 읽기

os.ReadFile()은 전체 파일을 메모리로 읽습니다(단순하지만 큰 파일에는 부적합). 큰 파일의 경우 os.Open()으로 열고, 청크로 읽고, 항상 defer Close()하세요. bufio.Scanner는 줄 단위로 읽습니다 — 텍스트 처리에 이상적. 에러를 항상 확인하세요, 특히 읽기가 끝났는지 알기 위해 io.EOF.

go
import (
    "io"
    "os"
)

// Read entire file (small files)
data, err := os.ReadFile("input.txt")
if err != nil {
    log.Fatal(err)
}
fmt.Println(string(data))

// Open and read (large files)
file, err := os.Open("large.txt")
if err != nil {
    log.Fatal(err)
}
defer file.Close()

// Read in chunks
buf := make([]byte, 1024)
for {
    n, err := file.Read(buf)
    if n > 0 {
        // process buf[:n]
    }
    if err == io.EOF {
        break
    }
    if err != nil {
        log.Fatal(err)
    }
}

// Read line by line
scanner := bufio.NewScanner(file)
for scanner.Scan() {
    line := scanner.Text()
    fmt.Println(line)
}

파일 쓰기

os.WriteFile()은 원자적으로 생성/잘라내기 및 쓰기를 합니다(단순). os.OpenFile()은 플래그로 제어를 제공합니다: O_APPEND(끝에 추가), O_CREATE(없으면 생성), O_TRUNC(자르기). 많은 작은 쓰기에는 bufio.Writer를 사용하세요(메모리에 버퍼, 끝에 flush). 파일 모드 0644: 소유자는 읽기/쓰기, 다른 사람은 읽기.

go
// Write entire file (creates or truncates)
err := os.WriteFile("output.txt", []byte("Hello, World!"), 0644)
if err != nil {
    log.Fatal(err)
}

// Open for writing (with options)
file, err := os.OpenFile("log.txt",
    os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
    log.Fatal(err)
}
defer file.Close()

// Write
if _, err := file.WriteString("log entry\n"); err != nil {
    log.Fatal(err)
}

// Buffered writer (efficient for many small writes)
writer := bufio.NewWriter(file)
for i := 0; i < 1000; i++ {
    writer.WriteString(fmt.Sprintf("line %d\n", i))
}
writer.Flush() // don't forget to flush!

// File modes: 0644 = owner read/write, others read
// os.O_APPEND, O_CREATE, O_WRONLY, O_RDONLY, O_RDWR, O_TRUNC

파일 작업

os.Mkdir/MkdirAll은 디렉토리를 만듭니다. os.ReadDir은 항목을 효율적으로 나열합니다. filepath.WalkDir은 디렉토리를 재귀적으로 순회합니다. os.Stat은 파일 정보(크기, 수정 시간, 권한)를 제공합니다. os.IsNotExist는 파일이 누락되었는지 검사합니다. os.Remove는 하나의 파일을 삭제하고; RemoveAll은 디렉토리를 재귀적으로 삭제합니다. 항상 에러를 확인하세요.

go
// Create directory
os.Mkdir("mydir", 0755)       // one level
os.MkdirAll("a/b/c", 0755)    // nested directories

// List directory
entries, err := os.ReadDir(".")
if err != nil {
    log.Fatal(err)
}
for _, entry := range entries {
    fmt.Println(entry.Name(), entry.IsDir())
}

// Walk directory tree
filepath.WalkDir(".", func(path string, d fs.DirEntry, err error) error {
    if err != nil {
        return err
    }
    fmt.Println(path)
    return nil
})

// File info
info, _ := os.Stat("file.txt")
fmt.Println(info.Size())      // bytes
fmt.Println(info.ModTime())   // last modified
fmt.Println(info.IsDir())

// Check existence
if _, err := os.Stat("file.txt"); os.IsNotExist(err) {
    fmt.Println("file does not exist")
}

// Remove
os.Remove("file.txt")
os.RemoveAll("directory") // recursive

환경 & 명령줄

os.Getenv/LookupEnv/Setenv는 환경 변수를 관리합니다. os.Args는 원시 명령줄 인수를 제공합니다. flag 패키지는 기본값과 도움말 텍스트가 있는 파싱된 플래그를 제공합니다. LookupEnv는 설정되지 않은 값과 빈 값을 구별합니다. 환경 변수는 12-factor 앱을 구성하는 표준 방법입니다(API 키, 데이터베이스 URL).

go
// Environment variables
os.Setenv("API_KEY", "secret")
key := os.Getenv("API_KEY") // "" if not set

// With existence check
if val, ok := os.LookupEnv("HOME"); ok {
    fmt.Println("HOME:", val)
}

// All environment variables
for _, e := range os.Environ() {
    fmt.Println(e)
}

// Command line arguments
// args := os.Args // []string, args[0] is program name

// Using flag package
var port int
var host string
flag.IntVar(&port, "port", 8080, "server port")
flag.StringVar(&host, "host", "localhost", "server host")
flag.Parse()
fmt.Printf("Running on %s:%d\n", host, port)

// Run: go run main.go -port 3000 -host 0.0.0.0

명령 실행

os/exec는 외부 명령을 실행합니다. Command()는 명령을 만들고; Output()은 stdout을 캡처하고; Run()은 사용자 정의 Stdout/Stderr로 실행합니다. 타임아웃을 위해 CommandContext를 사용하세요(프로세스 종료). 항상 에러를 확인하세요 — exec.ExitError는 0이 아닌 종료 코드를 나타냅니다. 명령 인젝션을 방지하기 위해 사용자 입력에 주의하세요.

go
import "os/exec"

// Run a command and get output
cmd := exec.Command("ls", "-la", "/tmp")
output, err := cmd.Output()
if err != nil {
    log.Fatal(err)
}
fmt.Println(string(output))

// Capture stdout and stderr separately
cmd = exec.Command("git", "status")
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err = cmd.Run()
fmt.Println("stdout:", stdout.String())
fmt.Println("stderr:", stderr.String())

// Pipe input to command
cmd = exec.Command("grep", "error")
cmd.Stdin = strings.NewReader("info\nerror\nwarn\n")
result, _ := cmd.Output()
fmt.Println(string(result)) // "error"

// Run with context (timeout)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
cmd = exec.CommandContext(ctx, "sleep", "10")
err = cmd.Run() // killed after 5s
10

JSON & 인코딩

JSON Marshal & Unmarshal

encoding/json은 Go 구조체와 JSON 간을 변환합니다. Marshal(구조체 → JSON), Unmarshal(JSON → 구조체). 구조체 태그(json:"name")는 필드 명명과 가시성을 제어합니다. omitempty는 제로/빈 값을 건너뜁니다. json:"-"는 필드를 완전히 제외합니다. 이것이 Go에서 API 요청/응답을 처리하는 표준 방법입니다.

go
import "encoding/json"

type User struct {
    ID       int    `json:"id"`
    Name     string `json:"name"`
    Email    string `json:"email,omitempty"` // omit if empty
    Password string `json:"-"`               // never serialize
    Age      int    `json:"age,omitempty"`
}

// Struct to JSON (marshal)
user := User{ID: 1, Name: "Alice", Age: 30}
data, err := json.Marshal(user)
// {"id":1,"name":"Alice","age":30}

// Pretty print
pretty, _ := json.MarshalIndent(user, "", "  ")

// JSON to struct (unmarshal)
jsonStr := `{"id":2,"name":"Bob","email":"[email protected]"}`
var u User
err := json.Unmarshal([]byte(jsonStr), &u)
fmt.Println(u.Name) // "Bob"

// Struct tags control field names:
// json:"name"        -> rename
// json:"name,omitempty" -> skip if zero value
// json:"-"           -> skip entirely
// json:",omitempty"  -> keep name, skip if empty

맵 & 슬라이스가 있는 JSON

동적 JSON(알 수 없는 구조)의 경우 map[string]any(또는 interface{})로 언마샬하세요. JSON 숫자는 float64가 됩니다 — 접근하려면 타입 어설션. json.Decoder/Encoder는 스트림(파일, HTTP 본문)과 효율적으로 작동합니다. 스키마를 알면 구조체 언마샬링을 사용하고; 유연한/동적 데이터에는 맵을 사용하세요.

go
// Parse arbitrary JSON into map[string]interface{}
jsonStr := `{"name":"Alice","scores":[90,85,92],"active":true}`

var result map[string]any
json.Unmarshal([]byte(jsonStr), &result)

// Type-assert to access values
name := result["name"].(string)
scores := result["scores"].([]any)
for _, s := range scores {
    fmt.Println(s.(float64)) // JSON numbers are float64
}

// Encode a map
m := map[string]any{
    "count": 42,
    "items": []string{"a", "b", "c"},
    "meta":  map[string]any{"page": 1},
}
data, _ := json.Marshal(m)

// Decode JSON stream
dec := json.NewDecoder(strings.NewReader(jsonStr))
var v map[string]any
dec.Decode(&v)

// Encode to stream
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", "  ")
enc.Encode(user)

사용자 정의 JSON 마샬링

사용자 정의 직렬화를 위해 MarshalJSON/UnmarshalJSON을 구현하세요. 이것은 계산된 필드, 대체 형식(Money를 '99.99 USD'로), 민감한 데이터 처리 및 시간 형식 지정에 유용합니다. UnmarshalJSON의 메서드 리시버는 구조체를 수정하기 위해 포인터여야 합니다. time.Time은 자동으로 RFC 3339 문자열로 직렬화됩니다.

go
// Implement MarshalJSON/UnmarshalJSON
type Money struct {
    Amount   float64
    Currency string
}

func (m Money) MarshalJSON() ([]byte, error) {
    return json.Marshal(map[string]any{
        "amount":   m.Amount,
        "currency": m.Currency,
        "display":  fmt.Sprintf("%.2f %s", m.Amount, m.Currency),
    })
}

func (m *Money) UnmarshalJSON(data []byte) error {
    var v struct {
        Amount   float64
        Currency string
    }
    if err := json.Unmarshal(data, &v); err != nil {
        return err
    }
    m.Amount = v.Amount
    m.Currency = v.Currency
    return nil
}

money := Money{Amount: 99.99, Currency: "USD"}
data, _ := json.Marshal(money)
// {"amount":99.99,"currency":"USD","display":"99.99 USD"}

// Time formatting
type Event struct {
    Time time.Time `json:"time"`
}
// time.Time marshals as RFC 3339 by default

HTTP 서버

net/http 패키지는 HTTP 서버를 구축합니다. http.HandleFunc는 핸들러를 등록합니다. http.ResponseWriter는 응답을 쓰고; *http.Request는 요청을 읽습니다. JSON API의 경우 Content-Type을 설정하고 json.NewEncoder(w).Encode(data)를 사용하세요. 표준 라이브러리는 프로덕션 준비 완료 — 단순 API에 프레임워크 필요 없음. 복잡한 라우팅에는 mux 라우터(gorilla/mux, chi)를 사용하세요.

go
import (
    "encoding/json"
    "net/http"
)

type Response struct {
    Status  string `json:"status"`
    Message string `json:"message"`
}

func main() {
    // Simple handler
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello, %s!", r.URL.Path[1:])
    })

    // JSON API endpoint
    http.HandleFunc("/api/user", func(w http.ResponseWriter, r *http.Request) {
        user := User{ID: 1, Name: "Alice"}
        w.Header().Set("Content-Type", "application/json")
        json.NewEncoder(w).Encode(user)
    })

    // Start server
    fmt.Println("Server running on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

// Request methods:
// r.Method  -> "GET", "POST", etc.
// r.URL.Query().Get("key") -> query params
// r.Body    -> request body (io.ReadCloser)

HTTP 클라이언트

http.Get/Post는 편리한 단축키입니다. 사용자 정의 헤더, 메서드 또는 본문의 경우 http.NewRequest + client.Do()를 사용하세요. 연결 누수를 피하기 위해 항상 defer resp.Body.Close()하세요. 클라이언트에 타임아웃을 설정하세요(기본값은 타임아웃 없음 — 위험). 프로덕션의 경우 http.Client를 재사용하세요(연결 풀링) 및 취소를 위해 context를 사용하세요.

go
// Simple GET
resp, err := http.Get("https://api.example.com/users")
if err != nil {
    log.Fatal(err)
}
defer resp.Body.Close()

body, err := io.ReadAll(resp.Body)
fmt.Println(string(body))

// Custom request with headers
req, err := http.NewRequest("GET", "https://api.example.com/data", nil)
req.Header.Set("Authorization", "Bearer token123")
req.Header.Set("Accept", "application/json")

resp, err := http.DefaultClient.Do(req)
defer resp.Body.Close()

// POST with JSON body
user := User{Name: "Alice"}
body, _ := json.Marshal(user)
resp, err := http.Post(
    "https://api.example.com/users",
    "application/json",
    bytes.NewBuffer(body),
)
defer resp.Body.Close()

// With timeout
client := &http.Client{Timeout: 10 * time.Second}
resp, err = client.Get("https://slow-api.example.com")
11

테스팅 & 벤치마킹

단위 테스트

테스트 파일은 _test.go로 끝나고, 테스트 함수는 Test로 시작합니다. 테이블 기반 테스트는 관용적 패턴입니다: 슬라이스에 테스트 케이스를 정의하고, t.Run으로 서브테스트(명명됨, 개별적으로 실행 가능)를 루프하세요. 실패에는 t.Errorf를 사용하고(계속), 치명적 실패에는 t.Fatalf를 사용하세요(중지). 상세 출력을 위해 'go test -v'로 실행하세요.

go
// math_test.go (file must end with _test.go)
package math

import "testing"

// Test function: func TestXxx(t *testing.T)
func TestAdd(t *testing.T) {
    result := Add(2, 3)
    expected := 5
    if result != expected {
        t.Errorf("Add(2, 3) = %d; want %d", result, expected)
    }
}

// Table-driven tests (idiomatic Go)
func TestAddTable(t *testing.T) {
    tests := []struct {
        name     string
        a, b     int
        expected int
    }{
        {"positive", 2, 3, 5},
        {"negative", -1, -1, -2},
        {"zero", 0, 0, 0},
        {"mixed", -5, 10, 5},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result := Add(tt.a, tt.b)
            if result != tt.expected {
                t.Errorf("Add(%d, %d) = %d; want %d",
                    tt.a, tt.b, result, tt.expected)
            }
        })
    }
}

// Run: go test -v
// Run specific: go test -run TestAddTable/positive

벤치마크

벤치마크 함수는 Benchmark로 시작하고 *testing.B를 사용합니다. b.N 루프는 안정적인 측정을 얻기 위해 런타임에 의해 조정됩니다. 'go test -bench=.'로 실행하세요. b.ResetTimer()는 설정 시간을 제외합니다. b.ReportAllocs()는 메모리 할당을 표시합니다. 개선이 유의미한지 검증하려면 'benchstat'로 구현을 비교하세요.

go
// Benchmark function: func BenchmarkXxx(b *testing.B)
func BenchmarkFibonacci(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Fibonacci(20)
    }
}

// Benchmark with allocations
func BenchmarkStringConcat(b *testing.B) {
    for i := 0; i < b.N; i++ {
        s := ""
        for j := 0; j < 100; j++ {
            s += "a"
        }
    }
}

// Run: go test -bench=.
// Output:
// BenchmarkFibonacci-8        300     4234567 ns/op
// BenchmarkStringConcat-8     200     6789012 ns/op   100 B/op   1 allocs/op

// Benchmark with setup
func BenchmarkProcess(b *testing.B) {
    data := setup() // setup not measured
    b.ResetTimer()  // start timing here
    for i := 0; i < b.N; i++ {
        process(data)
    }
}

// Memory allocations
func BenchmarkAllocs(b *testing.B) {
    b.ReportAllocs()
    for i := 0; i < b.N; i++ {
        make([]int, 100)
    }
}

테스트 헬퍼 & 모킹

t.Helper()는 스택 추적에서 헬퍼 함수를 건너뛰어 에러 메시지를 개선합니다. 인터페이스를 구현하여 모킹하세요(Go의 모킹 방식 — 모킹 프레임워크 필요 없음). t.Cleanup()은 정리 함수를 등록합니다(defer와 같지만 테스트 스코프). 복잡한 모킹을 위해 testify/assert와 mockery 또는 인터페이스에서 모킹을 생성하는 gomock을 사용하세요.

go
// t.Helper() marks helper functions (better error locations)
func assertEqual(t *testing.T, got, want int) {
    t.Helper()
    if got != want {
        t.Errorf("got %d, want %d", got, want)
    }
}

// Interfaces for mocking
type DataStore interface {
    Get(key string) (string, error)
}

type MockStore struct {
    data map[string]string
}

func (m *MockStore) Get(key string) (string, error) {
    if v, ok := m.data[key]; ok {
        return v, nil
    }
    return "", errors.New("not found")
}

func TestService(t *testing.T) {
    store := &MockStore{data: map[string]string{"key": "value"}}
    svc := NewService(store)
    result := svc.GetValue("key")
    assertEqual(t, result, "value")
}

// Subtests with t.Run for setup/teardown
func TestWithCleanup(t *testing.T) {
    t.Cleanup(func() {
        // runs after test (LIFO order)
        os.Remove("tempfile")
    })
    // test code
}

표준 라이브러리 하이라이트

Go의 표준 라이브러리는 포괄적입니다. time은 날짜/시간을 처리합니다(참고: 형식은 참조 시간 2006-01-02 사용). regexp는 패턴 매칭. context는 고루틴 간 취소/타임아웃. sync.Pool은 객체 재사용(GC 압력 감소). Go 1.21+은 제네릭 유틸리티가 있는 slices와 maps 패키지를 추가합니다.

go
// time package
now := time.Now()
future := now.Add(24 * time.Hour)
formatted := now.Format("2006-01-02 15:04:05")
parsed, _ := time.Parse("2006-01-02", "2024-01-15")

// regexp
re := regexp.MustCompile(`\d{4}-\d{2}-\d{2}`)
matches := re.FindString("date: 2024-01-15")
all := re.FindAllString("2024-01-15 and 2024-02-20", -1)

// context (cancellation, timeouts)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
go doWork(ctx)
// cancel() or timeout stops doWork

// sync.Pool (reuse objects)
var bufPool = sync.Pool{
    New: func() any { return new(bytes.Buffer) },
}
buf := bufPool.Get().(*bytes.Buffer)
defer bufPool.Put(buf)

// sort (Go 1.21+ slices package)
slices.Sort(nums)
slices.Contains(nums, 42)
slices.Reverse(nums)
12

고루틴 심층

고루틴 시작 & WaitGroup

고루틴은 Go 런타임에 의해 관리되는 경량 스레드입니다(OS 스레드가 아님) — 수십만 개를 생성할 수 있습니다. sync.WaitGroup은 고루틴 완료를 조정합니다: 시작 전 Add(1), 완료 시 Done()(defer 사용), 카운터가 0에 도달할 때까지 Wait()로 차단. 모든 고루틴이 동일한 카운터를 공유하도록 항상 WaitGroup에 포인터를 전달하세요.

go
package main

import (
    "fmt"
    "sync"
)

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done() // signal completion when function returns
    fmt.Printf("Worker %d started\n", id)
    // ... do work ...
    fmt.Printf("Worker %d done\n", id)
}

func main() {
    var wg sync.WaitGroup

    for i := 1; i <= 5; i++ {
        wg.Add(1) // increment counter before starting goroutine
        go worker(i, &wg)
    }

    wg.Wait() // block until all goroutines call Done()
    fmt.Println("All workers finished")
}

GOMAXPROCS & 스케줄링

GOMAXPROCS는 고루틴을 동시에 실행할 OS 스레드 수를 제어합니다 — CPU 코어 수가 기본값이며 거의 항상 최적입니다. Go는 M:N 스케줄링을 사용합니다: 적은 OS 스레드에 많은 고루틴. 블로킹 I/O 또는 채널 작업은 스케줄러가 동일한 스레드에서 다른 고루틴을 실행하게 합니다. runtime.Gosched()는 명시적으로 양보합니다. GOMAXPROCS를 조정할 필요는 거의 없습니다.

go
package main

import (
    "fmt"
    "runtime"
)

func main() {
    // GOMAXPROCS: number of OS threads available to run goroutines
    fmt.Println("GOMAXPROCS:", runtime.GOMAXPROCS(0)) // defaults to CPU cores

    // Set it manually (rarely needed — default is optimal)
    runtime.GOMAXPROCS(2)

    // Goroutines are multiplexed onto OS threads (M:N scheduling)
    // - Blocking syscalls (I/O) don't block other goroutines
    // - The scheduler steals work across threads
    // - A goroutine blocked on channel/IO yields its thread

    // Number of goroutines currently running
    go func() { runtime.Gosched() }() // yield to other goroutines
    fmt.Println("Goroutines:", runtime.NumGoroutine())
}

고루틴 누수 & 방지

고루틴 누수는 고루틴이 영원히 차단될 때 발생합니다(예: 아무도 읽지 않는 버퍼 없는 채널에 송신) — 가비지 컬렉션되지 않습니다. 누수를 방지하세요: 채널 버퍼링, 취소 채널/context와 함께 select 사용, 항상 종료 경로 제공. 누수된 고루틴은 메모리와 CPU를 축적합니다. 프로덕션에서 누수를 감지하려면 runtime.NumGoroutine()과 pprof를 사용하세요.

go
package main

// BAD: goroutine leaks — this goroutine never exits
func leaky() <-chan int {
    ch := make(chan int)
    go func() {
        ch <- 42 // blocks forever if nobody reads from ch
    }()
    return ch
}

// GOOD: use context for cancellation
func safe(ctx context.Context) <-chan int {
    ch := make(chan int, 1) // buffered — sender never blocks
    go func() {
        select {
        case ch <- 42:
        case <-ctx.Done(): // exit when context is cancelled
            return
        }
    }()
    return ch
}

// GOOD: always ensure senders can exit
// 1. Buffer the channel so send doesn't block
// 2. Use select with a done/cancel channel
// 3. Use context.WithCancel/WithTimeout for lifecycle control

sync.Once(일회성 초기화)

sync.Once는 모든 고루틴에서 함수가 정확히 한 번 실행되도록 보장합니다 — 스레드 안전 싱글톤과 지연 초기화를 구현하는 표준 방법입니다. 뮤텍스로 보호된 플래그 검사보다 효율적입니다. Do 메서드는 첫 번째 호출이 완료될 때까지 동시 호출자를 차단합니다. sync.Once는 sync.OnceValue(Go 1.21+)와 같은 많은 표준 라이브러리 패턴에서 내부적으로 사용됩니다.

go
package main

import (
    "fmt"
    "sync"
)

var (
    instance *Database
    once     sync.Once
)

type Database struct{ name string }

func GetDB() *Database {
    // sync.Once guarantees the function runs exactly once,
    // even if called from many goroutines simultaneously
    once.Do(func() {
        instance = &Database{name: "production"}
        fmt.Println("Database initialized")
    })
    return instance
}

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            GetDB() // "Database initialized" prints only once
        }()
    }
    wg.Wait()
}

고루틴이 있는 워커 풀

워커 풀 패턴은 채널에서 작업을 처리하는 고정된 수의 고루틴으로 동시성을 제한합니다. 이것은 무제한 고루틴 생성으로 인한 리소스 고갈을 방지합니다. 워커는 채널이 닫힐 때 종료되는 'for job := range jobs'를 사용합니다. close(jobs)는 모든 워커에게 중지하라고 신호합니다. 이것이 제한된 병렬성을 위한 정식 Go 동시성 패턴입니다.

go
package main

import (
    "fmt"
    "sync"
)

func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
    defer wg.Done()
    for job := range jobs { // exits when jobs channel is closed
        results <- job * job // process and send result
    }
}

func main() {
    jobs := make(chan int, 100)
    results := make(chan int, 100)
    var wg sync.WaitGroup

    // Start 3 workers
    for w := 1; w <= 3; w++ {
        wg.Add(1)
        go worker(w, jobs, results, &wg)
    }

    // Send 5 jobs
    for j := 1; j <= 5; j++ {
        jobs <- j
    }
    close(jobs) // signal workers to stop (range loop exits)

    wg.Wait()
    close(results)

    for r := range results {
        fmt.Println("Result:", r)
    }
}
13

채널 & Select

채널 기본(버퍼 없음 & 버퍼 있음)

버퍼 없는 채널(make(chan T))은 송신자와 수신자를 동기화합니다 — 수신자가 준비될 때까지 송신이 차단됩니다. 버퍼 있는 채널(make(chan T, n))은 수신자 없이 n개의 송신을 허용하여 송신자/수신자 타이밍을 분리합니다. 채널을 닫는 것은 송신자만 해야 합니다('더 이상 값 없음' 신호). 닫힌 채널에서 수신하면 ok=false로 제로 값을 반환합니다. Range 루프는 채널이 닫힐 때 종료됩니다.

go
package main

import "fmt"

func main() {
    // Unbuffered: send blocks until someone receives (synchronous)
    ch := make(chan int)
    go func() {
        ch <- 42 // blocks until main reads
    }()
    fmt.Println(<-ch) // 42

    // Buffered: send blocks only when buffer is full
    buf := make(chan int, 3)
    buf <- 1 // doesn't block (buffer has space)
    buf <- 2
    buf <- 3
    // buf <- 4 // would block — buffer full
    fmt.Println(<-buf) // 1 (FIFO)

    // Close a channel (sender closes, never the receiver)
    close(buf)

    // Check if closed (ok is false when closed and empty)
    val, ok := <-buf
    fmt.Println(val, ok) // 2 true

    // Range over channel until closed
    nums := make(chan int, 3)
    nums <- 10; nums <- 20; nums <- 30
    close(nums)
    for n := range nums {
        fmt.Println(n) // 10, 20, 30
    }
}

Select 문

select는 고루틴이 여러 채널 작업을 동시에 대기하게 합니다 — 준비된 첫 번째 case를 선택합니다(여러 개면 무작위). default case는 select를 비차단으로 만듭니다. time.After는 타임아웃 채널을 만듭니다. select는 Go 동시성 조정의 핵심입니다: 멀티플렉싱, 타임아웃, 취소 및 fan-in/fan-out 패턴이 모두 그 위에 구축됩니다.

go
package main

import (
    "fmt"
    "time"
)

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)

    go func() {
        time.Sleep(1 * time.Second)
        ch1 <- "from ch1"
    }()
    go func() {
        time.Sleep(2 * time.Second)
        ch2 <- "from ch2"
    }()

    // select waits on multiple channel operations
    // picks the first one that's ready (random if multiple ready)
    for i := 0; i < 2; i++ {
        select {
        case msg := <-ch1:
            fmt.Println(msg)
        case msg := <-ch2:
            fmt.Println(msg)
        }
    }

    // Non-blocking receive with default
    select {
    case msg := <-ch1:
        fmt.Println(msg)
    default:
        fmt.Println("no message ready") // runs immediately
    }

    // Timeout with time.After
    select {
    case msg := <-ch1:
        fmt.Println(msg)
    case <-time.After(500 * time.Millisecond):
        fmt.Println("timed out")
    }
}

Fan-In & Fan-Out 패턴

Fan-out은 병렬성을 위해 작업을 여러 고루틴에 분산시키고; fan-in은 여러 채널을 하나로 병합합니다. 함께 병렬 파이프라인을 형성합니다: fan-out 워커가 독립적으로 처리한 다음 fan-in이 결과를 수집합니다. fan-in은 모든 입력 채널이 소진된 후에만 병합된 채널을 닫기 위해 WaitGroup을 사용합니다. 이 패턴들은 Go의 동시성 데이터 처리의 기초입니다.

go
package main

import (
    "fmt"
    "sync"
)

// Fan-Out: distribute work across multiple goroutines
func fanOut(input <-chan int, n int) []<-chan int {
    outputs := make([]<-chan int, n)
    for i := 0; i < n; i++ {
        outputs[i] = process(input) // each worker reads from same input
    }
    return outputs
}

func process(input <-chan int) <-chan int {
    output := make(chan int)
    go func() {
        defer close(output)
        for v := range input {
            output <- v * v // transform
        }
    }()
    return output
}

// Fan-In: merge multiple channels into one
func fanIn(channels ...<-chan int) <-chan int {
    var wg sync.WaitGroup
    merged := make(chan int)

    output := func(c <-chan int) {
        defer wg.Done()
        for v := range c {
            merged <- v
        }
    }

    wg.Add(len(channels))
    for _, c := range channels {
        go output(c)
    }

    go func() {
        wg.Wait()
        close(merged)
    }()
    return merged
}

방향성 채널(송신 전용/수신 전용)

방향성 채널 타입(수신 전용 <-chan T, 송신 전용 chan<- T)은 컴파일 타임에 채널 사용을 강제합니다. 양방향 채널은 함수에 전달될 때 방향성 타입으로 암시적 변환됩니다. 이것은 의도를 문서화하고 버그를 방지합니다 — 생산자 함수는 자체 출력 채널에서 수신할 수 없습니다. 계약을 명확하게 하기 위해 함수 시그니처에서 방향성 타입을 사용하세요.

go
package main

import "fmt"

// <-chan int: receive-only (can read, cannot send)
// chan<- int: send-only (can send, cannot read)
func producer(out chan<- int) { // can only send
    for i := 0; i < 3; i++ {
        out <- i
    }
    close(out)
}

func consumer(in <-chan int) { // can only receive
    for v := range in {
        fmt.Println("consumed:", v)
    }
}

func main() {
    ch := make(chan int)

    // Directional restrictions are enforced at compile time
    go producer(ch) // bidirectional chan converts to send-only
    consumer(ch)    // bidirectional chan converts to receive-only

    // This prevents bugs: producer can't accidentally read,
    // consumer can't accidentally send or close
}

Ticker & Timer 채널

time.Ticker는 간격마다 반복적으로 발생합니다 — 주기적 작업을 위해 select에서 ticker.C를 채널로 사용하세요. time.Timer는 기간 후 한 번 발생합니다. 리소스를 해제하고 누수를 방지하기 위해 항상 ticker와 timer에서 Stop()을 호출하세요. time.After는 편리한 일회성 타이머로 채널을 반환합니다(단, 취소할 수 없으므로 축적을 방지하기 위해 select 루프에서는 NewTimer를 선호). Reset으로 타이머를 재예약할 수 있습니다.

go
package main

import (
    "fmt"
    "time"
)

func main() {
    // Ticker: fires repeatedly at intervals
    ticker := time.NewTicker(500 * time.Millisecond)
    defer ticker.Stop() // always stop to release resources

    go func() {
        for t := range ticker.C { // ticker.C is a channel
            fmt.Println("Tick at", t)
        }
    }()

    // Timer: fires once after a duration
    timer := time.NewTimer(2 * time.Second)
    <-timer.C // blocks until timer fires
    fmt.Println("Timer fired!")

    // time.After: one-shot timer as a channel (no Stop needed)
    select {
    case <-time.After(1 * time.Second):
        fmt.Println("1 second elapsed")
    }

    // Reset a timer (cancel and reschedule)
    timer2 := time.NewTimer(5 * time.Second)
    timer2.Reset(100 * time.Millisecond)
    <-timer2.C
}
14

Context 패키지

context.WithCancel & WithTimeout

context.Context는 고루틴 경계에 걸쳐 취소, 타임아웃 및 요청 스코프 값을 전달합니다. WithCancel은 context와 cancel 함수를 반환하고; WithTimeout은 기간 후 자동 취소됩니다. 고루틴은 중지 시기를 알기 위해 select에서 ctx.Done()(채널)을 검사합니다. 타임아웃이 발생해도 항상 cancel 함수를 호출하세요(defer 사용) — 그렇지 않으면 context가 누수됩니다.

go
package main

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

func worker(ctx context.Context, id int) {
    for {
        select {
        case <-ctx.Done(): // cancelled or timed out
            fmt.Printf("Worker %d stopped: %v\n", id, ctx.Err())
            return
        default:
            fmt.Printf("Worker %d working...\n", id)
            time.Sleep(500 * time.Millisecond)
        }
    }
}

func main() {
    // WithCancel: manual cancellation
    ctx, cancel := context.WithCancel(context.Background())
    go worker(ctx, 1)
    time.Sleep(2 * time.Second)
    cancel() // stop the worker
    time.Sleep(500 * time.Millisecond)

    // WithTimeout: auto-cancel after duration
    ctx2, cancel2 := context.WithTimeout(context.Background(), 1500*time.Millisecond)
    defer cancel2() // always call cancel to release resources
    go worker(ctx2, 2)
    time.Sleep(2 * time.Second) // worker stops after 1.5s
}

호출을 통한 Context 전파

Context는 I/O를 수행하는 모든 함수의 첫 번째 매개변수여야 하며, 전체 호출 체인을 통해 전파되어야 합니다. http.Request.Context()는 클라이언트가 연결 해제될 때 자동으로 취소됩니다. ctx를 데이터베이스/HTTP 작업(QueryRowContext, NewRequestWithContext)에 전달하면 context가 취소될 때 중단되어 불필요한 작업과 리소스 누수를 방지합니다. context를 struct에 저장하지 마세요.

go
package main

import (
    "context"
    "database/sql"
    "net/http"
)

// Context should be the FIRST parameter, named ctx
func GetUser(ctx context.Context, db *sql.DB, id int) (string, error) {
    // Pass ctx to all blocking operations so they cancel together
    var name string
    err := db.QueryRowContext(ctx, "SELECT name FROM users WHERE id=?", id).Scan(&name)
    return name, err
}

func handler(w http.ResponseWriter, r *http.Request) {
    // r.Context() is cancelled when the client disconnects
    ctx := r.Context()

    name, err := GetUser(ctx, db, 42)
    if err != nil {
        http.Error(w, err.Error(), 500)
        return
    }
    w.Write([]byte(name))
}

// BEST PRACTICE: pass context through every function in the call chain
// that might do I/O. This ensures a client disconnect or timeout
// cancels ALL in-flight work (DB queries, HTTP calls, etc.)

Context 값(요청 스코프 데이터)

context.WithValue는 호출 체인을 통해 흐르는 요청 스코프 데이터(사용자 ID, 추적 ID, 인증 토큰)를 저장합니다. 키 충돌을 피하기 위해 사용자 정의 키 타입(문자열이 아닌)을 사용하세요. 값은 함수 매개변수가 아닌 요청에 필요한 데이터여야 합니다 — Go 팀은 주로 추적/인증과 같은 횡단 관심사에 대해 드물게 사용할 것을 권장합니다. 값 검색 시 항상 타입 어설션을 하세요.

go
package main

import (
    "context"
    "fmt"
    "net/http"
)

// Define a custom key type to avoid collisions
type contextKey string

const userIDKey contextKey = "userID"

// Set a value in context
func authMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        userID := 42 // authenticate...
        // WithValue creates a new context with the value
        ctx := context.WithValue(r.Context(), userIDKey, userID)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

// Get a value from context (must type-assert)
func getUserID(ctx context.Context) (int, bool) {
    if v, ok := ctx.Value(userIDKey).(int); ok {
        return v, true
    }
    return 0, false
}

func handler(w http.ResponseWriter, r *http.Request) {
    if uid, ok := getUserID(r.Context()); ok {
        fmt.Fprintf(w, "User ID: %d", uid)
    }
}

Context로 우아한 종료

우아한 종료는 서버가 종료되기 전에 진행 중인 요청이 완료되도록 합니다. signal.Notify는 OS 신호(Ctrl+C, 컨테이너 오케스트레이터의 SIGTERM)를 캡처합니다. server.Shutdown(ctx)는 새 연결 수락을 중지하고 활성 연결이 완료될 때까지 대기합니다(context 타임아웃까지). 이것은 프로덕션 서버에 필수적입니다 — 없으면 활성 요청이 갑자기 종료되어 에러와 데이터 손상이 발생합니다.

go
package main

import (
    "context"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"
)

func main() {
    server := &http.Server{Addr: ":8080", Handler: mux}

    // Listen for OS interrupt signals (Ctrl+C, SIGTERM)
    stop := make(chan os.Signal, 1)
    signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)

    go func() {
        log.Println("Server starting on :8080")
        if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatalf("Server error: %v", err)
        }
    }()

    <-stop // block until signal received
    log.Println("Shutting down...")

    // Give in-flight requests 10 seconds to finish
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    if err := server.Shutdown(ctx); err != nil {
        log.Printf("Forced shutdown: %v", err)
    }
    log.Println("Server stopped gracefully")
}

Context 마감 & 에러 처리

context.WithDeadline은 절대 시간에 취소하고; WithTimeout은 상대 기간 후 취소합니다(WithTimeout은 단지 WithDeadline(now+timeout)). ctx.Err()는 context.DeadlineExceeded 또는 context.Canceled를 반환하여 중지 이유를 구별할 수 있게 합니다. context 에러를 검사하려면 errors.Is()를 사용하세요. 긴 작업 시작 시 항상 ctx.Err()를 확인하고 블로킹 대기 중 select와 ctx.Done()을 사용하세요.

go
package main

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

func slowOperation(ctx context.Context) (string, error) {
    // Check if already cancelled before starting
    if err := ctx.Err(); err != nil {
        return "", err
    }

    select {
    case <-time.After(3 * time.Second): // simulate slow work
        return "result", nil
    case <-ctx.Done(): // cancelled or deadline exceeded
        return "", ctx.Err()
    }
}

func main() {
    // Deadline: cancel at a specific time
    deadline := time.Now().Add(1 * time.Second)
    ctx, cancel := context.WithDeadline(context.Background(), deadline)
    defer cancel()

    result, err := slowOperation(ctx)
    if err != nil {
        if errors.Is(err, context.DeadlineExceeded) {
            fmt.Println("Operation timed out")
        } else if errors.Is(err, context.Canceled) {
            fmt.Println("Operation was cancelled")
        }
        return
    }
    fmt.Println(result)
}
15

HTTP 서버 & 클라이언트

HTTP 서버(net/http)

net/http는 프로덕션 준비 완료 HTTP 서버를 제공합니다. http.HandleFunc는 경로별로 핸들러를 등록합니다. JSON 응답을 쓰려면 json.NewEncoder(w).Encode()를 사용하고 요청 본문을 파싱하려면 json.NewDecoder(r.Body).Decode()를 사용하세요. 항상 Content-Type 헤더를 설정하고 r.Method를 확인하세요. http.Error는 에러 상태를 보냅니다. ListenAndServe가 서버를 시작합니다; 에러를 잡으려면 log.Fatal로 감싸세요.

go
package main

import (
    "encoding/json"
    "log"
    "net/http"
)

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

func getUser(w http.ResponseWriter, r *http.Request) {
    user := User{ID: 1, Name: "Alice"}

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(user)
}

func createUser(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        return
    }

    var user User
    if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
        http.Error(w, "Invalid JSON", http.StatusBadRequest)
        return
    }
    user.ID = 99
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(user)
}

func main() {
    http.HandleFunc("/users/1", getUser)
    http.HandleFunc("/users", createUser)

    log.Println("Server on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

HTTP 클라이언트 & 요청

기본 http.Client에는 타임아웃이 없습니다 — 응답하지 않는 서버에서 영원히 대기하는 것을 피하기 위해 항상 하나를 설정하세요. 취소/타임아웃을 위해 context를 연결하려면 http.NewRequestWithContext를 사용하세요. 연결 누수를 피하기 위해 항상 defer resp.Body.Close()하세요. 프로덕션에서는 단일 http.Client를 재사용하세요(연결 풀링 관리). http.Get은 단축키이지만 타임아웃과 사용자 정의가 부족합니다.

go
package main

import (
    "bytes"
    "context"
    "encoding/json"
    "fmt"
    "net/http"
    "time"
)

func main() {
    // Simple GET
    resp, err := http.Get("https://api.example.com/users")
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close() // ALWAYS close the body

    var users []map[string]any
    json.NewDecoder(resp.Body).Decode(&users)
    fmt.Println(users)

    // Custom client with timeout (default has NO timeout!)
    client := &http.Client{Timeout: 30 * time.Second}

    // POST with JSON body and context
    body, _ := json.Marshal(map[string]string{"name": "Bob"})
    req, _ := http.NewRequestWithContext(context.Background(),
        "POST", "https://api.example.com/users", bytes.NewReader(body))
    req.Header.Set("Content-Type", "application/json")

    resp, err = client.Do(req)
    defer resp.Body.Close()
    fmt.Println("Status:", resp.Status)
}

미들웨어 패턴

미들웨어는 핸들러 자체를 수정하지 않고 횡단 관심사(로깅, 인증, CORS, 속도 제한)를 추가하기 위해 핸들러를 감쌉니다. 시그니처 func(http.Handler) http.Handler가 표준 미들웨어 타입입니다. Chain은 순서대로 적용합니다(가장 바깥부터). 이 패턴은 Chi, Echo, Gin 같은 프레임워크의 기초입니다. ResponseWriter를 감싸서 상태 코드를 캡처할 수 있습니다.

go
package main

import (
    "log"
    "net/http"
    "time"
)

// Middleware wraps an http.Handler to add cross-cutting concerns
type Middleware func(http.Handler) http.Handler

func Logging(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        next.ServeHTTP(w, r) // call the wrapped handler
        log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
    })
}

func Auth(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if r.Header.Get("Authorization") == "" {
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return
        }
        next.ServeHTTP(w, r)
    })
}

// Chain multiple middlewares
func Chain(h http.Handler, mws ...Middleware) http.Handler {
    for i := len(mws) - 1; i >= 0; i-- {
        h = mws[i](h)
    }
    return h
}

func main() {
    handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello!"))
    })
    // Apply middlewares: Auth -> Logging -> handler
    http.Handle("/", Chain(handler, Logging, Auth))
    http.ListenAndServe(":8080", nil)
}

정적 파일 & 템플릿 서빙

http.FileServer는 디렉토리에서 정적 파일을 서빙합니다; http.StripPrefix는 파일 경로가 올바르게 해결되도록 경로 접두사를 제거합니다. html/template은 HTML을 안전하게 렌더링합니다(XSS 방지를 위해 자동 이스케이프). template.Must은 파싱 에러 시 패닉(시작 시 괜찮음). 템플릿은 데이터에 {{.Field}}, 이터레이션에 {{range}}를 사용합니다. 프로덕션에서는 디스크에서 읽는 대신 go:embed로 파일을 임베드하는 것을 고려하세요.

go
package main

import (
    "html/template"
    "net/http"
)

func main() {
    // Serve static files (CSS, JS, images)
    fs := http.FileServer(http.Dir("./static"))
    http.Handle("/static/", http.StripPrefix("/static/", fs))

    // HTML templates
    tmpl := template.Must(template.ParseFiles("templates/index.html"))

    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        data := struct {
            Title string
            Items []string
        }{
            Title: "My Page",
            Items: []string{"Apple", "Banana", "Cherry"},
        }
        tmpl.Execute(w, data)
    })

    http.ListenAndServe(":8080", nil)
}

// templates/index.html:
// <h1>{{.Title}}</h1>
// <ul>{{range .Items}}<li>{{.}}</li>{{end}}</ul>

우아한 종료 & go:embed

go:embed는 빌드 시점에 컴파일된 바이너리에 파일을 번들합니다 — 외부 파일 의존성이 없는 진정한 단일 바이너리 배포를 가능하게 합니다. var 선언 위에 //go:embed 지시문을 사용하세요. embed.FS는 읽기 전용 가상 파일 시스템입니다. fs.Sub은 하위 파일 시스템을 만듭니다(디렉토리 접두사 제거). 이것은 정적 자산, HTML 템플릿, SQL 마이그레이션, 설정 파일 등에 작동합니다.

go
package main

import (
    "embed"
    "io/fs"
    "net/http"
)

//go:embed static/*
var staticFiles embed.FS

func main() {
    // Embed static files into the binary (no external files needed)
    // Access the embedded filesystem
    sub, _ := fs.Sub(staticFiles, "static")
    http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(sub))))

    // go:embed benefits:
    // - Single binary deployment (no external files)
    // - Files are compiled into the executable
    // - Works with templates, configs, migrations, etc.

    // Embed a single file
    //go:embed config.json
    // var configData []byte

    // Embed with pattern
    //go:embed templates/*.html
    // var templates embed.FS

    http.ListenAndServe(":8080", nil)
}
16

Sync 패키지

sync.Mutex & sync.RWMutex

sync.Mutex는 배타적 잠금을 제공합니다 — 한 번에 하나의 고루틴만 보유 가능. sync.RWMutex는 여러 동시 읽기 또는 하나의 배타적 쓰기를 허용합니다 — 읽기가 쓰기를 훨씬 초과할 때 사용하세요. 함수가 패닉될 경우 교착 상태를 방지하기 위해 항상 Lock과 defer Unlock을 짝지으세요. 뮤텍스를 구조체에 임베드하세요(소문자 mu)하여 private로 유지. 뮤텍스를 절대 복사하지 마세요(항상 포인터 사용).

go
package main

import (
    "fmt"
    "sync"
)

type SafeCounter struct {
    mu    sync.Mutex
    count int
}

func (c *SafeCounter) Increment() {
    c.mu.Lock()
    defer c.mu.Unlock() // always unlock with defer
    c.count++
}

func (c *SafeCounter) Value() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.count
}

// RWMutex: allows multiple readers OR one writer
type SafeCache struct {
    mu   sync.RWMutex
    data map[string]string
}

func (c *SafeCache) Get(key string) (string, bool) {
    c.mu.RLock()         // read lock (multiple readers OK)
    defer c.mu.RUnlock()
    val, ok := c.data[key]
    return val, ok
}

func (c *SafeCache) Set(key, val string) {
    c.mu.Lock()          // write lock (exclusive)
    defer c.mu.Unlock()
    c.data[key] = val
}

sync.Map(동시 맵)

sync.Map은 특정 시나리오에 최적화된 동시 안전 맵입니다: 한 번 쓰기 여러 번 읽기(캐시) 또는 고루틴 간 분리된 키 접근. 원자적 연산을 통해 읽기 시 잠금을 피합니다. 단, 일반적인 동시 맵의 경우 sync.RWMutex로 보호된 일반 맵이 종종 더 빠르고 인체공학적입니다. sync.Map의 API는 키와 값에 any(interface{})를 사용하여 타입 안전성을 잃습니다.

go
package main

import (
    "fmt"
    "sync"
)

func main() {
    var m sync.Map

    // Store and Load (thread-safe, no locks needed)
    m.Store("name", "Alice")
    m.Store("age", 30)

    val, ok := m.Load("name")
    fmt.Println(val, ok) // Alice true

    // LoadOrStore: atomic get-or-set
    actual, loaded := m.LoadOrStore("name", "Bob")
    fmt.Println(actual, loaded) // Alice true (already existed)

    // Delete
    m.Delete("age")

    // Range over all entries
    m.Range(func(key, value any) bool {
        fmt.Printf("%v = %v\n", key, value)
        return true // continue; return false to stop
    })

    // Use sync.Map when:
    // 1. Keys are written once, read many times (caches)
    // 2. Multiple goroutines read/write disjoint keys
    // For most cases, a regular map + Mutex is simpler and faster
}

sync.Cond(조건 변수)

sync.Cond는 조건 변수를 구현합니다 — 고루틴이 조건이 참이 될 때까지 대기. Wait()은 원자적으로 잠금을 해제하고 대기; Signal()은 하나의 대기자를 깨우고, Broadcast()는 모두 깨웁니다. 허위 웨이크업을 처리하기 위해 Wait() 주위에 항상 for 루프를 사용하세요(if가 아님). Cond는 생산자-소비자 큐와 상태 변경 대기에 유용하지만, 채널이 종종 더 단순한 대안을 제공합니다.

go
package main

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

type Queue struct {
    items []int
    mu    sync.Mutex
    cond  *sync.Cond
}

func NewQueue() *Queue {
    q := &Queue{}
    q.cond = sync.NewCond(&q.mu) // cond is tied to the mutex
    return q
}

func (q *Queue) Put(item int) {
    q.mu.Lock()
    defer q.mu.Unlock()
    q.items = append(q.items, item)
    q.cond.Signal() // wake one waiting goroutine
}

func (q *Queue) Get() int {
    q.mu.Lock()
    defer q.mu.Unlock()
    for len(q.items) == 0 {
        q.cond.Wait() // atomically unlocks mu, sleeps, re-locks on wake
    }
    item := q.items[0]
    q.items = q.items[1:]
    return item
}

func main() {
    q := NewQueue()
    go func() {
        time.Sleep(1 * time.Second)
        q.Put(42)
    }()
    fmt.Println("Waiting for item...")
    fmt.Println("Got:", q.Get()) // blocks until item available
}

sync.Pool(객체 재사용)

sync.Pool은 객체를 재사용하여 할당과 GC 압력을 줄입니다 — bytes.Buffer 같이 자주 할당되는 수명이 짧은 객체에 이상적. Get()은 풀링된 객체를 반환하고(비어 있으면 New 호출); Put()은 반환합니다. GC 중에 풀이 비워지므로 지속성에 의존하지 마세요. 재사용 전에 항상 객체를 재설정하세요. 표준 라이브러리는 sync.Pool을 광범위하게 사용합니다(http, json, fmt).

go
package main

import (
    "bytes"
    "sync"
)

var bufPool = sync.Pool{
    New: func() any {
        return new(bytes.Buffer) // create when pool is empty
    },
}

func Process(data []byte) string {
    // Get a buffer from the pool (or create via New)
    buf := bufPool.Get().(*bytes.Buffer)
    defer bufPool.Put(buf) // return it when done

    buf.Reset() // clear before reuse
    buf.Write(data)
    buf.WriteString("-processed")
    return buf.String()
}

// Benefits:
// - Reduces GC pressure by reusing objects
// - Avoids allocation overhead for short-lived objects
// - Pool entries can be garbage collected between GC cycles
//
// Use for: bytes.Buffer, json encoders, temporary slices
// NOT for: long-lived objects or when size is unpredictable

errgroup(에러 전파가 있는 그룹)

errgroup(golang.org/x/sync에서)은 첫 번째 에러를 반환하고 context를 통해 나머지 고루틴을 취소할 수 있는 WaitGroup입니다. g.Go()는 고루틴을 시작하고; g.Wait()은 차단하고 첫 번째 nil이 아닌 에러를 반환합니다. WithContext는 어떤 고루틴이 에러를 반환하면 취소되는 context를 만듭니다 — 다른 고루틴이 일찍 중지합니다. 이것이 모두 성공하거나 함께 실패해야 하는 병렬 작업을 실행하는 관용적 방법입니다.

go
package main

import (
    "context"
    "fmt"
    "golang.org/x/sync/errgroup"
    "net/http"
)

// errgroup is like WaitGroup but propagates the first error
// and can cancel remaining goroutines on failure
func fetchAll(urls []string) ([]*http.Response, error) {
    g, ctx := errgroup.WithContext(context.Background())
    results := make([]*http.Response, len(urls))

    for i, url := range urls {
        i, url := i, url // capture loop variables
        g.Go(func() error {
            req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
            resp, err := http.DefaultClient.Do(req)
            if err != nil {
                return err // cancels ctx, stopping other goroutines
            }
            results[i] = resp
            return nil
        })
    }

    if err := g.Wait(); err != nil {
        return nil, err // first error returned
    }
    return results, nil
}

func main() {
    urls := []string{
        "https://example.com",
        "https://golang.org",
    }
    resps, err := fetchAll(urls)
    fmt.Println(len(resps), err)
}
17

제네릭(Go 1.18+)

제네릭 함수 & 타입 매개변수

Go 제네릭(1.18+)은 대괄호 안에 타입 매개변수를 사용합니다: [T any]는 T를 모든 타입으로 선언합니다. 컴파일러가 인수에서 타입을 추론하므로 거의 명시적으로 지정할 필요가 없습니다. 제네릭은 interface{}와 타입 어설션 없이 Map/Filter/Reduce 같은 타입 안전 재사용 가능 함수를 가능하게 합니다. 'any'는 제네릭과 함께 도입된 interface{}의 별칭입니다.

go
package main

import "fmt"

// Type parameters in brackets: [T any] means T can be any type
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, predicate func(T) bool) []T {
    var result []T
    for _, v := range slice {
        if predicate(v) {
            result = append(result, v)
        }
    }
    return result
}

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

func main() {
    nums := []int{1, 2, 3, 4, 5}
    squared := Map(nums, func(n int) int { return n * n })
    fmt.Println(squared) // [1 4 9 16 25]

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

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

    // Works with any type
    words := []string{"go", "rust", "python"}
    lengths := Map(words, func(s string) int { return len(s) })
    fmt.Println(lengths) // [2 4 6]
}

타입 제약 & comparable

타입 제약은 제네릭이 허용하는 타입을 제한합니다. 'comparable'은 ==을 지원하는 타입의 내장 제약입니다(맵 키와 비교에 필요). 사용자 정의 제약은 타입 union(int | float64)을 사용하여 특정 타입을 허용합니다. golang.org/x/exp/constraints 패키지는 Ordered(<, > 등용)를 제공합니다. 제약은 타입 집합과 메서드를 결합할 수 있습니다.

go
package main

import "fmt"

// comparable: types that support == and != (built-in constraint)
func Contains[T comparable](slice []T, target T) bool {
    for _, v := range slice {
        if v == target {
            return true
        }
    }
    return false
}

// Custom constraint with a type set
type Number interface {
    int | int64 | float64 | float32
}

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

// Multiple type parameters
func Pair[T, U any](first T, second U) struct{ First T; Second U } {
    return struct{ First T; Second U }{first, second}
}

func main() {
    fmt.Println(Contains([]string{"a", "b", "c"}, "b")) // true
    fmt.Println(Contains([]int{1, 2, 3}, 5))             // false

    fmt.Println(Sum([]int{1, 2, 3}))           // 6
    fmt.Println(Sum([]float64{1.5, 2.5}))      // 4

    p := Pair("Alice", 30)
    fmt.Println(p) // {Alice 30}
}

제네릭 데이터 구조

제네릭 타입(Stack[T any])은 interface{} 박싱이나 타입 어설션 없이 모든 타입과 작동하는 타입 안전 데이터 구조를 만듭니다. 타입 매개변수 T는 타입의 일부이므로 Stack[int]와 Stack[string]은 별개의 컴파일 타임 검사 타입입니다. 이것은 런타임 타입 에러의 전체 클래스를 제거합니다. 제네릭 구조체, 메서드 및 인터페이스 모두 타입 매개변수를 지원합니다.

go
package main

import "fmt"

// Generic Stack — works with any type, type-safe
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) {
    if len(s.items) == 0 {
        var zero T
        return zero, false
    }
    index := len(s.items) - 1
    item := s.items[index]
    s.items = s.items[:index]
    return item, true
}

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

func main() {
    // Type-safe stack of ints
    intStack := &Stack[int]{}
    intStack.Push(1)
    intStack.Push(2)
    val, _ := intStack.Pop()
    fmt.Println(val) // 2

    // Type-safe stack of strings
    strStack := &Stack[string]{}
    strStack.Push("hello")
    s, _ := strStack.Pop()
    fmt.Println(s) // hello

    // intStack.Push("oops") // compile error! type-safe
}

~(기본 타입)가 있는 제네릭 제약

제약에서 ~ 접두사는 기본 타입이 명명된 타입인 모든 타입과 일치합니다 — 따라서 ~string은 string과 type MyString string 모두와 일치합니다. ~이 없으면 제약은 정확한 명명된 타입만 일치하며, 이는 사용자 정의 타입에 거의 유용하지 않습니다. 제네릭이 타입 별칭 및 원시에서 파생된 명명된 타입과 작동하기를 원할 때 ~(도메인 모델링에서 일반적)을 사용하세요.

go
package main

import "fmt"

// ~ allows any type whose UNDERLYING type matches
// (e.g., type MyString string would match ~string)
type StringLike interface {
    ~string
}

func Join[S StringLike](items []S, sep string) string {
    result := ""
    for i, item := range items {
        if i > 0 {
            result += sep
        }
        result += string(item) // convert underlying type to string
    }
    return result
}

// Numeric constraint with ~
type Integer interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64
}

func Double[T Integer](v T) T {
    return v * 2
}

// Custom type based on int
type Score int

func main() {
    type MyString string
    items := []MyString{"a", "b", "c"}
    fmt.Println(Join(items, "-")) // a-b-c

    s := Double(Score(21))
    fmt.Println(s) // 42
}

제네릭 vs 인터페이스(사용 시기)

알고리즘이 타입 간에 동일할 때(컬렉션, 수학, 변환) 제네릭을 선택하세요 — 런타임 오버헤드 없이 컴파일 타임 타입 안전성을 제공합니다. 서로 다른 타입이 서로 다른 구현이 필요하거나(다형성) 런타임 디스패치가 필요할 때(의존성 주입, 모킹) 인터페이스를 선택하세요. 둘은 보완적입니다: 제네릭은 인터페이스로 제약될 수 있습니다(T Stringer) — 두 세계의 장점을 모두.

go
package main

import "fmt"

// GENERICS: best for algorithms/data structures that work
// the same way regardless of type (collections, sorting, mapping)
func Max[T int | float64](a, b T) T {
    if a > b {
        return a
    }
    return b
}

// INTERFACES: best when different types need DIFFERENT implementations
// (polymorphism, dependency injection, mocking)
type Stringer interface {
    String() string
}

type Dog struct{ Name string }
func (d Dog) String() string { return "Dog: " + d.Name }

type Cat struct{ Name string }
func (c Cat) String() string { return "Cat: " + c.Name }

func Print[T Stringer](v T) {
    fmt.Println(v.String())
}

func main() {
    fmt.Println(Max(3, 7))       // 7
    fmt.Println(Max(3.14, 2.71)) // 3.14

    Print(Dog{"Rex"}) // Dog: Rex
    Print(Cat{"Whiskers"}) // Cat: Whiskers
}

// RULE OF THUMB:
// - Same logic, different types -> Generics
// - Different logic, shared contract -> Interfaces
// - Need runtime polymorphism -> Interfaces
// - Need compile-time type safety -> Generics
18

리플렉션

reflect.Type & reflect.Value

reflect.TypeOf는 런타임 타입을 반환하고; reflect.ValueOf는 런타임 값을 반환합니다. Type에서 구조체 필드, 메서드 및 태그를 검사할 수 있습니다. Value에서 필드를 읽고(CanSet인 경우) 수정할 수 있습니다. 리플렉션은 강력하지만 느리고(직접 접근보다 10-100배 느림) 컴파일 타임 타입 안전성을 우회합니다 — 주로 직렬화, ORM 및 프레임워크에 드물게 사용하세요.

go
package main

import (
    "fmt"
    "reflect"
)

type User struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}

func main() {
    u := User{Name: "Alice", Age: 30}

    // Get type and value at runtime
    t := reflect.TypeOf(u)
    v := reflect.ValueOf(u)

    fmt.Println("Type:", t.Name())        // User
    fmt.Println("Kind:", t.Kind())         // struct
    fmt.Println("NumFields:", t.NumField())

    // Iterate struct fields
    for i := 0; i < t.NumField(); i++ {
        field := t.Field(i)
        value := v.Field(i)
        tag := field.Tag.Get("json")
        fmt.Printf("%s (%s) = %v [json:%s]\n",
            field.Name, field.Type, value, tag)
    }
    // Name (string) = Alice [json:name]
    // Age (int) = 30 [json:age]
}

리플렉션으로 값 수정

리플렉션으로 값을 수정하려면 포인터를 전달하고 .Elem()을 호출하여 역참조해야 합니다. CanSet()은 필드가 할당 가능한지(주소 가능한 값의 내보내진 필드) 보고합니다. 내보내지 않은(소문자) 필드는 읽을 수 있지만 설정할 수 없습니다. SetInt/SetString/Set은 런타임에 타입 검사되며 불일치 시 패닉합니다. 리플렉션 기반 변이는 설정 파서와 ORM의 기초입니다.

go
package main

import (
    "fmt"
    "reflect"
)

func main() {
    type Config struct {
        Port int
        Host string
    }

    cfg := Config{Port: 8080, Host: "localhost"}

    // MUST pass a pointer to modify (ValueOf of a struct is a copy)
    v := reflect.ValueOf(&cfg).Elem()

    // Check if a field is settable
    portField := v.FieldByName("Port")
    fmt.Println("Settable:", portField.CanSet()) // true

    // Modify fields by name
    portField.SetInt(9090)
    v.FieldByName("Host").SetString("0.0.0.0")

    fmt.Println(cfg) // {9090 0.0.0.0}

    // Set via interface (type-checked at runtime)
    v.FieldByName("Port").Set(reflect.ValueOf(7070))
    fmt.Println(cfg) // {7070 0.0.0.0}

    // Unexported fields are NOT settable (CanSet returns false)
}

동적으로 메서드 호출

리플렉션은 이름으로 메서드를 동적으로 호출할 수 있습니다 — RPC 프레임워크, 플러그인 시스템 및 라우팅에 유용. MethodByName은 Value를 반환하고; Call()은 reflect.Value 인수 슬라이스로 호출하고 결과 슬라이스를 반환합니다. NumMethod/Method는 메서드를 열거합니다. 리플렉션을 통한 메서드 호출은 느리고 타입 안전성을 우회하므로 메서드 이름이 컴파일 타임에 알려지지 않은 경우에만 사용하세요.

go
package main

import (
    "fmt"
    "reflect"
)

type Calculator struct{}

func (c Calculator) Add(a, b int) int { return a + b }
func (c Calculator) Mul(a, b int) int { return a * b }

func main() {
    calc := Calculator{}
    v := reflect.ValueOf(calc)

    // Find a method by name
    method := v.MethodByName("Add")
    fmt.Println("Found:", method.IsValid()) // true

    // Call with arguments (each arg must be a reflect.Value)
    args := []reflect.Value{
        reflect.ValueOf(3),
        reflect.ValueOf(4),
    }
    results := method.Call(args)
    fmt.Println("3 + 4 =", results[0].Int()) // 7

    // List all methods
    t := reflect.TypeOf(calc)
    for i := 0; i < t.NumMethod(); i++ {
        m := t.Method(i)
        fmt.Printf("Method: %s, Type: %v\n", m.Name, m.Type)
    }
    // Method: Add, Type: func(main.Calculator, int, int) int
    // Method: Mul, Type: func(main.Calculator, int, int) int
}

실용적 사용: 구조체에서 맵으로(태그 기반)

이 패턴 — 구조체 필드를 반복하고 태그를 읽는 것 — 이 encoding/json, YAML 파서, ORM 및 검증 라이브러리가 내부적으로 작동하는 방식입니다. reflect를 사용하면 태그를 기반으로 모든 구조체를 처리하는 제네릭 코드를 작성할 수 있습니다. json:'-' 태그 관례(이 필드 건너뛰기)는 표준입니다. 이것은 Go에서 가장 정당한 리플렉션 사용 중 하나입니다.

go
package main

import (
    "fmt"
    "reflect"
)

// Convert any struct to a map using struct tags as keys
func StructToMap(obj any, tag string) map[string]any {
    result := make(map[string]any)
    v := reflect.ValueOf(obj)
    t := reflect.TypeOf(obj)

    // Dereference pointers
    if v.Kind() == reflect.Ptr {
        v = v.Elem()
        t = t.Elem()
    }

    for i := 0; i < t.NumField(); i++ {
        field := t.Field(i)
        // Use the tag value if present, else the field name
        key := field.Tag.Get(tag)
        if key == "" {
            key = field.Name
        }
        // Skip fields tagged with "-"
        if key == "-" {
            continue
        }
        result[key] = v.Field(i).Interface()
    }
    return result
}

type User struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
    Pass string `json:"-"` // excluded
}

func main() {
    u := User{Name: "Alice", Age: 30, Pass: "secret"}
    m := StructToMap(u, "json")
    fmt.Println(m) // map[age:30 name:Alice]
}
19

빌드 & 툴링

go build, go run & go install

go build는 실행 파일로 컴파일하고; go run은 임시 파일로 컴파일하고 실행합니다(개발에 적합); go install은 바이너리를 전역 접근을 위해 $GOPATH/bin에 넣습니다. Go의 돋보이는 기능은 GOOS/GOARCH를 통한 쉬운 교차 컴파일입니다 — 툴체인 필요 없음. -ldflags='-s -w'는 ~30% 더 작은 바이너리를 위해 디버그 정보를 제거합니다. -X는 CI/CD를 위해 빌드 시점에 값(버전 문자열 같은)을 주입합니다.

go
# Build an executable (outputs to current directory)
go build -o myapp              # custom output name
go build -o bin/myapp ./cmd    # build a specific package

# Build for different OS/arch (cross-compilation)
GOOS=linux GOARCH=amd64 go build -o myapp-linux
GOOS=windows GOARCH=amd64 go build -o myapp.exe
GOOS=darwin GOARCH=arm64 go build -o myapp-mac  # Apple Silicon

# Run without building an executable (compiles to temp, runs, deletes)
go run main.go                 # run single file
go run ./cmd/server            # run a package

# Install: builds and puts binary in $GOPATH/bin
go install ./cmd/myapp         # now 'myapp' is on your PATH

# Build flags
go build -v                    # verbose: print packages being compiled
go build -race                 # enable race detector (for testing)
go build -ldflags="-s -w"      # strip debug info (smaller binary)
go build -ldflags="-X main.Version=1.0.0"  # inject version at build time

go mod(모듈 관리)

go mod는 go.mod(의존성 목록)와 go.sum(보안을 위한 체크섬)으로 의존성을 관리합니다. go get은 의존성을 추가/업그레이드하고; go mod tidy는 파일을 동기화합니다(커밋 전 실행). Go는 Semantic Import Versioning을 사용합니다: v2+는 /v2 경로 접미사가 필요합니다. 모듈 캐시는 $GOPATH/pkg/mod에서 공유됩니다. go mod vendor는 재현 가능하고 오프라인이거나 감사된 빌드를 위해 vendor/ 디렉토리를 만듭니다.

go
# Initialize a new module
go mod init github.com/user/myproject

# This creates go.mod:
# module github.com/user/myproject
# go 1.21

# Add a dependency (auto-added to go.mod)
go get github.com/gin-gonic/gin@latest
go get github.com/lib/[email protected]  # specific version

# Tidy: add missing deps, remove unused ones
go mod tidy

# Download dependencies to local cache
go mod download

# Verify dependencies haven't been modified
go mod verify

# Vendor: copy deps into ./vendor (for offline builds)
go mod vendor

# Upgrade dependencies
go get -u github.com/lib/pq       # upgrade to latest minor
go get -u=patch github.com/lib/pq # upgrade patches only
go get github.com/lib/[email protected]  # pin specific version

# View dependency graph
go mod graph | head

go test & 벤치마킹

go test는 _test.go 파일을 실행합니다; TestXxx(t *testing.T)라는 함수는 테스트, BenchmarkXxx(b *testing.B)는 벤치마크입니다. -race는 데이터 레이스 디텍터를 활성화합니다(동시성 코드에 필수). -cover는 테스트 커버리지를 표시하고; -coverprofile은 상세 보고서를 생성합니다. 벤치마크는 N이 자동 조정되는 b.N 반복을 실행합니다. 테이블 기반 테스트(입력/예상 슬라이스)는 관용적 Go 테스팅 스타일입니다.

go
# Run all tests in the current package
go test ./...

# Verbose output
go test -v ./...

# Run a specific test
go test -run TestAdd -v

# Run benchmarks
go test -bench=. -benchmem

# Race detector (find data races)
go test -race ./...

# Coverage
go test -cover ./...
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out  # view in browser

# --- test file example (xxx_test.go) ---
# func TestAdd(t *testing.T) {
#     got := Add(2, 3)
#     if got != 5 {
#         t.Errorf("Add(2,3) = %d, want 5", got)
#     }
# }
#
# func BenchmarkAdd(b *testing.B) {
#     for i := 0; i < b.N; i++ {
#         Add(2, 3)
#     }
# }

go fmt, go vet & golangci-lint

go fmt/gofmt는 단일 정식 형식을 강제합니다 — Go에는 형식 논쟁이 없습니다. go vet은 일반적인 버그를 잡습니다(printf 불일치, 락 복사, 잘못된 구조체 태그). golangci-lint는 수십 개의 린터를 집계하며 CI의 업계 표준입니다. 매 커밋 전에 gofmt와 go vet을 실행하고; 더 깊은 분석을 위해 CI에 golangci-lint를 추가하세요. 일관된 툴링은 Go 코드베이스가 균일해 보이는 주요 이유입니다.

go
# go fmt: format code (the ONE true Go style)
go fmt ./...              # format all files
gofmt -d main.go          # show diff without changing
gofmt -w main.go          # write changes in place

# go vet: static analysis for common mistakes
go vet ./...              # check all packages
# Catches: printf format mismatches, unreachable code,
# struct tag errors, lock copies, shadowed variables

# golangci-lint: meta-linter (runs many linters)
# Install: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
golangci-lint run         # run all enabled linters
golangci-lint run --enable=gosec,goconst  # enable specific linters

# Common linters:
# errcheck    - check unchecked errors
# gosec       - security issues
# govet       - go vet checks
# staticcheck - advanced static analysis
# ineffassign - detect ineffectual assignments
# unused      - find unused code

# goimports: fmt + auto-manage imports
# go install golang.org/x/tools/cmd/goimports@latest
goimports -w main.go

프로파일링 & pprof

pprof는 Go의 내장 프로파일러입니다 — CPU, 메모리, 고루틴 및 뮤텍스 프로파일링. 장기 실행 서버의 경우, 재시작 없이 라이브 프로파일링을 위해 /debug/pprof/ 엔드포인트를 노출하려면 _ 'net/http/pprof'를 임포트하세요. go tool pprof는 대화형 셸(top, list, web) 또는 웹 UI를 제공합니다. 실행 트레이서(go tool trace)는 고루틴 스케줄링과 블로킹을 시각화합니다. 성능이 중요한 Go 코드에 프로파일링은 필수적입니다.

go
# CPU profiling
go test -cpuprofile=cpu.prof -bench=.
go tool pprof cpu.prof
# Interactive commands: top, list FunctionName, web (graphviz)

# Memory profiling
go test -memprofile=mem.prof -bench=.
go tool pprof mem.prof

# Built-in HTTP pprof endpoint (for production servers)
import _ "net/http/pprof"
# Then visit: http://localhost:8080/debug/pprof/
# Capture a 30-second CPU profile:
# go tool pprof http://localhost:8080/debug/pprof/profile?seconds=30

# In code:
import "runtime/pprof"
f, _ := os.Create("cpu.prof")
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()
// ... code to profile ...

# Trace (execution tracer)
go test -trace=trace.out -bench=.
go tool trace trace.out  # opens in browser
20

데이터베이스(SQL)

database/sql 기본

database/sql은 SQL 데이터베이스용 표준 인터페이스입니다. 드라이버는 부수 효과(자체 등록)를 위해 임포트됩니다. sql.Open은 연결을 설정하지 않습니다; 검증하려면 Ping을 사용하세요. 항상 defer db.Close()하세요.

go
import "database/sql"
import _ "github.com/lib/pq"
db, err := sql.Open("postgres", "host=localhost dbname=mydb")
if err != nil { log.Fatal(err) }
defer db.Close()
err = db.Ping()

행 쿼리

Query는 여러 행을 반환합니다. 리소스를 해제하려면 항상 defer rows.Close()하세요. Scan은 열 값을 변수로 복사합니다. 이터레이션 에러를 위해 루프 후 rows.Err()를 확인하세요.

go
rows, err := db.Query("SELECT id, name FROM users WHERE age > $1", 18)
if err != nil { log.Fatal(err) }
defer rows.Close()
for rows.Next() {
    var id int; var name string
    if err := rows.Scan(&id, &name); err != nil { log.Fatal(err) }
    fmt.Printf("%d: %s\n", id, name)
}

단일 행 쿼리

QueryRow는 단일 행을 반환합니다. 일치하는 행이 없으면 Scan은 sql.ErrNoRows를 반환합니다. 단일 행 조회를 위해 Query보다 깔끔합니다. 항상 ErrNoRows를 명시적으로 처리하세요.

go
var name string
err := db.QueryRow("SELECT name FROM users WHERE id = $1", 1).Scan(&name)
if err != nil {
    if err == sql.ErrNoRows { fmt.Println("Not found") } else { log.Fatal(err) }
}

준비된 문

Prepare는 재사용 가능한 문을 만들어 반복 쿼리의 성능을 향상시킵니다. SQL 인젝션을 방지합니다. 항상 defer stmt.Close()하세요. 여러 번 실행되는 쿼리에 사용하세요.

go
stmt, err := db.Prepare("INSERT INTO users(name) VALUES($1)")
if err != nil { log.Fatal(err) }
defer stmt.Close()
_, err = stmt.Exec("Alice")
_, err = stmt.Exec("Bob")

트랜잭션

Begin은 트랜잭션을 시작합니다. tx 내의 모든 작업은 원자적입니다. defer Rollback은 안전합니다: Commit 후에는 no-op입니다. 어떤 작업이든 실패하면 Rollback이 모든 변경을 취소합니다.

go
tx, err := db.Begin()
if err != nil { log.Fatal(err) }
defer tx.Rollback()  // Safe to call after commit
_, err = tx.Exec("UPDATE accounts SET bal = bal - 100 WHERE id = 1")
_, err = tx.Exec("UPDATE accounts SET bal = bal + 100 WHERE id = 2")
if err != nil { log.Fatal(err) }
err = tx.Commit()
21

테스팅 심층

기본 테스트

테스트 함수는 Test로 시작하고 *testing.T를 받습니다. t.Errorf는 실패를 기록하고 계속합니다. t.Fatalf는 기록하고 중지합니다. go test로 실행하세요. 더 깔끔한 어설션을 위해 testify/assert를 사용하세요.

go
func TestAdd(t *testing.T) {
    got := Add(2, 3)
    want := 5
    if got != want {
        t.Errorf("Add(2,3) = %d; want %d", got, want)
    }
}

테이블 기반 테스트

테이블 기반 테스트는 Go에서 관용적입니다. 테스트 케이스를 구조체 슬라이스로 정의하세요. 루프를 통해 각각 실행하세요. t.Run은 개별 실패를 위한 서브테스트를 만듭니다.

go
func TestAdd(t *testing.T) {
    tests := []struct {
        a, b, want int
    }{
        {1, 2, 3}, {-1, 1, 0}, {0, 0, 0},
    }
    for _, tt := range tests {
        got := Add(tt.a, tt.b)
        if got != tt.want {
            t.Errorf("Add(%d,%d)=%d; want %d", tt.a, tt.b, got, tt.want)
        }
    }
}

서브테스트

서브테스트는 이름과 함수가 있는 t.Run을 사용합니다. -run 패턴으로 특정 서브테스트를 실행하세요. 더 나은 테스트 조직과 출력을 제공합니다.

go
func TestAdd(t *testing.T) {
    t.Run("positive", func(t *testing.T) {
        if Add(1, 2) != 3 { t.Error("failed") }
    })
    t.Run("negative", func(t *testing.T) {
        if Add(-1, -2) != -3 { t.Error("failed") }
    })
}
// Run specific: go test -run TestAdd/negative

Test Main

TestMain은 패키지에 대해 한 번 실행되며 기본 테스트 러너를 대체합니다. 전역 설정/정리에 사용하세요. 테스트를 실행하려면 m.Run()을 호출해야 합니다. os.Exit은 종료 코드를 전파합니다.

go
func TestMain(m *testing.M) {
    setup()
    code := m.Run()
    teardown()
    os.Exit(code)
}
func setup() { /* initialize DB, etc. */ }
func teardown() { /* cleanup */ }

인터페이스 모킹

Go 모킹은 인터페이스에 의존합니다. 인터페이스를 정의하고, 모킹을 구현하고, 주입하세요. mockery와 mockgen 같은 도구는 모킹을 자동 생성합니다. 외부 의존성 없이 단위 테스트를 가능하게 합니다.

go
type Store interface { Get(id int) (string, error) }
type MockStore struct { data map[int]string }
func (m *MockStore) Get(id int) (string, error) {
    if v, ok := m.data[id]; ok { return v, nil }
    return "", errors.New("not found")
}
// Use in tests by injecting MockStore
22

벤치마킹

기본 벤치마크

벤치마크 함수는 Benchmark로 시작하고 *testing.B를 받습니다. b.N은 안정적인 측정을 얻기 위해 프레임워크에 의해 조정됩니다. go test -bench로 실행하세요. 출력은 ns/op(작업당 나노초)를 표시합니다.

go
func BenchmarkAdd(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Add(2, 3)
    }
}
// Run: go test -bench=.
// Output: BenchmarkAdd-8    1000000000    0.3 ns/op

서브 벤치마크

서브 벤치마크는 b.Run을 사용합니다. ResetTimer는 설정 시간을 제외합니다. ReportAllocs는 메모리 할당을 표시합니다. 입력 크기에 따른 성능을 비교하세요.

go
func BenchmarkSort(b *testing.B) {
    sizes := []int{100, 1000, 10000}
    for _, size := range sizes {
        b.Run(fmt.Sprintf("size-%d", size), func(b *testing.B) {
            data := generateData(size)
            b.ResetTimer()
            for i := 0; i < b.N; i++ {
                Sort(data)
            }
        })
    }
}

메모리 할당

ReportAllocs는 작업당 메모리 할당을 표시합니다. 할당 감소는 Go 성능의 핵심입니다. sync.Pool을 사용하고, 슬라이스를 미리 할당하고, 불필요한 문자열 연결을 피하세요.

go
func BenchmarkConcat(b *testing.B) {
    b.ReportAllocs()
    for i := 0; i < b.N; i++ {
        s := "a" + "b" + "c"
        _ = s
    }
}
// Output includes allocs/op and B/op

병렬 벤치마크

RunParallel은 여러 고루틴에 걸쳐 벤치마크를 동시에 실행합니다. 스레드 안전 코드 테스트에 유용합니다. pb.Next()는 작업 분배를 반복합니다. 동시성 하에서 처리량을 측정합니다.

go
func BenchmarkParallel(b *testing.B) {
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            Add(2, 3)
        }
    })
}

벤치마크 비교

benchstat는 벤치마크 결과를 통계적으로 비교합니다. 신뢰할 수 있는 비교를 위해 -count=10으로 여러 번 실행하세요. 성능 개선을 검증하고 회귀를 감지하는 데 도움이 됩니다.

go
# Run with memory stats
benchstat old.txt new.txt
# Compare two runs
# name      old time/op  new time/op  delta
# Add-8     2.3ns        1.8ns        -21.7%
23

프로파일링(pprof)

CPU 프로파일링

StartCPUProfile은 CPU 프로파일을 파일에 씁니다. StopCPUProfile은 데이터를 플러시합니다. go tool pprof로 분석하세요. pprof에서 top, list, web 명령을 사용하세요. 가장 많은 CPU를 소비하는 함수에 집중하세요.

go
import "runtime/pprof"
f, _ := os.Create("cpu.prof")
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()
// Run your code here
// Analyze: go tool pprof cpu.prof

메모리 프로파일링

WriteHeapProfile은 현재 힙 상태를 캡처합니다. 정확한 결과를 위해 먼저 runtime.GC()를 호출하세요. 모든 할당을 프로파일링하려면 MemProfileRate = 1을 설정하세요(느리지만 정확).

go
// Add to code
runtime.GC()
f, _ := os.Create("mem.prof")
pprof.WriteHeapProfile(f)
f.Close()
// Or use runtime.MemProfileRate = 1 for all allocations

HTTP 프로파일링

net/http/pprof는 기본 mux에 프로파일링 엔드포인트를 등록합니다. 재시작 없이 HTTP로 프로파일에 접근하세요. 프로덕션 진단에 유용합니다. 프로덕션에서 엔드포인트를 보호하세요.

go
import _ "net/http/pprof"
go func() {
    log.Println(http.ListenAndServe("localhost:6060", nil))
}()
// Analyze live:
// go tool pprof http://localhost:6060/debug/pprof/profile

pprof 명령

top은 가장 많은 리소스를 소비하는 함수를 표시합니다. list는 주석이 달린 소스 코드를 표시합니다. web은 SVG 호출 그래프를 엽니다. tree는 호출 계층을 표시합니다. 필터링에 focus를 사용하세요.

go
# Start interactive pprof
go tool pprof cpu.prof
(pprof) top 10        # Top functions by CPU
(pprof) list Add      # Show source with timings
(pprof) web           # Open graph in browser
(pprof) tree          # Call tree
(pprof) png > out.png # Save graph as PNG

트레이스

runtime/trace는 실행 트레이스를 캡처합니다: 고루틴 스케줄링, GC, syscall 블로킹. go tool trace는 웹 UI를 엽니다. 지연 및 동시성 문제 진단에 유용합니다.

go
import "runtime/trace"
f, _ := os.Create("trace.out")
trace.Start(f)
defer trace.Stop()
// Run code
// View: go tool trace trace.out
24

빌드 태그

기본 빌드 태그

빌드 태그는 파일을 조건부로 컴파일합니다. //go:build 구문(Go 1.17+)이 // +build를 대체합니다. 태그는 OS(linux, darwin, windows), arch(amd64, arm64) 또는 사용자 정의일 수 있습니다.

go
//go:build linux
// +build linux

package main
// This file only compiles on Linux

여러 태그

&&(이전 구문에서 쉼표)는 모든 태그가 필요합니다. ||(이전 구문에서 공백)는 어느 하나의 태그가 필요합니다. !는 부정입니다. 플랫폼별 구현에 사용하세요.

go
//go:build linux && amd64
// +build linux,amd64

//go:build linux || darwin
// +build linux darwin

//go:build !windows
// +build !windows

사용자 정의 태그

사용자 정의 태그는 선택적 기능을 가능하게 합니다. -tags 플래그로 빌드하세요. 일반적 사용: 디버그 빌드, 실험적 기능, 엔터프라이즈 vs 커뮤니티 에디션. 태그 이름을 소문자로 유지하세요.

go
//go:build debug
// +build debug

package main
const DebugMode = true
// Build with: go build -tags debug

파일 접미사

파일 접미사는 암시적 빌드 제약을 제공합니다. OS용 _linux, _darwin, _windows. 아키텍처용 _amd64, _arm64. 플랫폼별 코드에 태그보다 단순합니다.

go
// file: utils_linux.go    -> Linux only
// file: utils_darwin.go   -> macOS only
// file: utils_windows.go  -> Windows only
// file: utils_amd64.go    -> amd64 only
// file: utils_debug.go    -> requires -tags debug

태그로 빌드

-tags로 조건부 컴파일을 활성화하세요. 여러 태그는 공백으로 구분됩니다. Go 버전 태그(go1.18)는 버전별 코드를 활성화합니다. go list -tags로 확인하세요.

go
# Build with debug tag
go build -tags debug
# Multiple tags
go build -tags "debug verbose"
# List available tags
go list -tags ./...
# Conditional in code
//go:build go1.18  // Requires Go 1.18+
25

CGO 기본

기본 CGO

CGO는 Go에서 C를 호출할 수 있게 합니다. C 코드는 import "C" 위 주석에 있습니다. 임포트는 주석 바로 뒤에 있어야 합니다. CGO는 빌드를 느리게 하고 교차 컴파일을 방지합니다.

go
/*
#include <stdio.h>
void hello() {
    printf("Hello from C!\n");
}
*/
import "C"
func main() {
    C.hello()
}

문자열 전달

C.CString은 C 문자열을 할당합니다(C.free로 해제해야 함). CString은 데이터를 복사합니다. 변환에는 unsafe.Pointer를 사용하세요. 누수를 방지하기 위해 C 할당 메모리를 항상 해제하세요.

go
/*
#include <string.h>
int len(const char* s) { return strlen(s); }
*/
import "C"
import "unsafe"
s := "Hello"cs := C.CString(s)
defer C.free(unsafe.Pointer(cs))
length := C.len(cs)

C 라이브러리 호출

#cgo LDFLAGS는 C 라이브러리를 링크합니다. #cgo CFLAGS는 컴파일러 플래그를 설정합니다. #include로 시스템 헤더를 포함하세요. CGO는 Go와 libm, libcrypto 같은 기존 C 라이브러리를 연결합니다.

go
/*
#cgo LDFLAGS: -lm
#include <math.h>
*/
import "C"
result := float64(C.sqrt(16.0))
fmt.Println(result)  // 4

C에서 Go로 콜백

//export로 내보낸 Go 함수는 C에서 호출할 수 있습니다. 함수는 package main에 있어야 합니다. C 라이브러리가 Go로 콜백할 수 있게 합니다. FFI 바인딩에 사용됩니다.

go
/*
extern void goCallback(int);
void callGo(int n) { goCallback(n); }
*/
import "C"
//export goCallback
func goCallback(n C.int) {
    fmt.Printf("Called with %d\n", n)
}

성능 참고

CGO 호출은 Go 함수 호출에 비해 상당한 오버헤드가 있습니다. 성능이 중요한 코드에서는 피하세요. 경계 간 호출을 줄이기 위해 작업을 일괄 처리하세요. 순수 Go 빌드를 위해 CGO_ENABLED=0을 설정하세요.

go
// CGO calls have overhead (~100ns)
// Avoid in hot paths
// Batch C calls to amortize cost
// CGO disables inlining and some optimizations
// Build: CGO_ENABLED=1 go build
26

웹 프레임워크

net/http 서버

net/http는 표준 라이브러리 HTTP 서버입니다. HandleFunc가 핸들러를 등록합니다. ListenAndServe가 서버를 시작합니다. 단순 앱에는 기본 mux가 충분합니다; 프로덕션에는 사용자 정의 mux를 사용하세요.

go
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello, %s!", r.URL.Path[1:])
})
http.HandleFunc("/api", apiHandler)
log.Fatal(http.ListenAndServe(":8080", nil))

Gin 프레임워크

Gin은 고성능 HTTP 프레임워크입니다. Param은 URL 매개변수를 추출합니다. JSON은 응답을 직렬화합니다. Gin은 라우팅, 미들웨어 및 JSON 검증을 제공합니다. httprouter 덕분에 net/http보다 빠릅니다.

go
import "github.com/gin-gonic/gin"
r := gin.Default()
r.GET("/users/:id", func(c *gin.Context) {
    id := c.Param("id")
    c.JSON(200, gin.H{"user": id})
})
r.Run(":8080")

미들웨어

미들웨어는 핸들러를 감싸 횡단 관심사를 추가합니다: 로깅, 인증, CORS. c.Next()는 다음 핸들러를 호출합니다. gin.Recovery()는 패닉으로 인한 충돌을 방지합니다. 순서가 중요합니다.

go
func Logger() gin.HandlerFunc {
    return func(c *gin.Context) {
        start := time.Now()
        c.Next()
        fmt.Printf("%s %s %v\n", c.Request.Method, c.URL.Path, time.Since(start))
    }
}
r := gin.New()
r.Use(Logger(), gin.Recovery())

Echo 프레임워크

Echo는 Gin과 유사한 또 다른 인기 있는 프레임워크입니다. 핸들러는 중앙화된 에러 처리를 위해 에러를 반환합니다. CORS, JWT, 속도 제한을 위한 내장 미들웨어. 깔끔한 API 설계.

go
import "github.com/labstack/echo/v4"
e := echo.New()
e.GET("/users/:id", func(c echo.Context) error {
    id := c.Param("id")
    return c.JSON(200, map[string]string{"user": id})
})
e.Start(":8080")

정적 파일

FileServer는 정적 파일을 서빙합니다. StripPrefix는 URL 경로를 조정합니다. HTML, CSS, JS 및 이미지 서빙에 유용합니다. 프로덕션에서는 정적 자산을 위해 CDN이나 nginx를 사용하세요.

go
// net/http
fs := http.FileServer(http.Dir("./static"))
http.Handle("/static/", http.StripPrefix("/static/", fs))
// Gin
r.Static("/assets", "./assets")
r.StaticFile("/favicon.ico", "./favicon.ico")
27

gRPC

Proto 정의

Protocol Buffers는 서비스 계약을 정의합니다. proto3가 최신 구문입니다. service는 RPC 메서드를 정의합니다. message는 데이터 구조를 정의합니다. protoc로 Go 코드를 생성하세요.

go
syntax = "proto3";
package greet;
service Greeter {
    rpc SayHello (HelloRequest) returns (HelloReply) {}
}
message HelloRequest { string name = 1; }
message HelloReply { string message = 1; }

코드 생성

protoc는 .proto 파일에서 Go 코드를 생성합니다. --go_out은 메시지 타입을 생성합니다. --go-grpc_out은 서비스 스텁을 생성합니다. 생성된 코드는 수동으로 편집되지 않습니다.

go
# Install protoc and plugins
protoc --go_out=. --go-grpc_out=. \
    greet.proto
# This generates greet.pb.go and greet_grpc.pb.go
# Containing message types and service interfaces

서버 구현

순방향 호환성을 위해 UnimplementedGreeterServer를 임베드하세요. 서비스 메서드를 구현하세요. grpc.NewServer가 서버를 만듭니다. 서빙 전에 서비스를 등록하세요.

go
type server struct { greet.UnimplementedGreeterServer }
func (s *server) SayHello(ctx context.Context, in *greet.HelloRequest) (*greet.HelloReply, error) {
    return &greet.HelloReply{Message: "Hello " + in.Name}, nil
}
lis, _ := net.Listen("tcp", ":50051")
grpc.NewServer().Serve(lis)

클라이언트

grpc.Dial이 연결을 설정합니다. WithInsecure는 TLS를 비활성화합니다(프로덕션에는 credentials.NewTLS 사용). 클라이언트 스텁은 타입화된 메서드를 제공합니다. 연결은 풀링되고 재사용됩니다.

go
conn, _ := grpc.Dial("localhost:50051", grpc.WithInsecure())
client := greet.NewGreeterClient(conn)
resp, _ := client.SayHello(context.Background(), &greet.HelloRequest{Name: "Alice"})
fmt.Println(resp.Message)

스트리밍

gRPC는 세 가지 스트리밍 패턴을 지원합니다. stream 키워드가 스트리밍을 표시합니다. 서버 스트리밍: 하나의 요청, 여러 응답. 양방향: 양측이 스트림. 실시간 데이터에 유용합니다.

go
// Server streaming
rpc LotsOfReplies(HelloRequest) returns (stream HelloReply);
// Client streaming
rpc LotsOfGreetings(stream HelloRequest) returns (HelloReply);
// Bidirectional
rpc BidiHello(stream HelloRequest) returns (stream HelloReply);
28

에러 래핑

에러 래핑

%w 동사를 사용하여 원본을 보존하면서 에러를 래핑하세요. 이것은 에러 체인을 만듭니다. 체인을 잃으므로 래핑에 %v를 피하세요. 래핑은 근본 원인을 잃지 않고 컨텍스트를 추가합니다.

go
if err != nil {
    return fmt.Errorf("failed to open config: %w", err)
}

언래핑

errors.Is는 체인의 어느 에러가 일치하는지 검사합니다. errors.As는 체인에서 특정 에러 타입을 추출합니다. 센티넬 값에는 Is를, 타입화된 에러에는 As를 사용하세요. 둘 다 래핑 체인을 순회합니다.

go
err := someOperation()
if errors.Is(err, sql.ErrNoRows) {
    // Handle not found
}
var pathErr *fs.PathError
if errors.As(err, &pathErr) {
    fmt.Println("Path:", pathErr.Path)
}

사용자 정의 에러

사용자 정의 에러 타입은 error 인터페이스를 구현합니다. 에러 처리를 위한 구조화된 데이터를 전달합니다. errors.As로 사용자 정의 타입을 추출하세요. 문자열 비교보다 타입화된 에러를 선호하세요.

go
type ValidationError struct {
    Field string
    Msg   string
}
func (e *ValidationError) Error() string {
    return fmt.Sprintf("%s: %s", e.Field, e.Msg)
}
func validate(s string) error {
    if s == "" { return &ValidationError{Field: "name", Msg: "required"} }
    return nil
}

센티넬 에러

센티넬 에러는 패키지 수준 에러 변수입니다. 예상되는 에러 조건에 사용하세요. errors.Is로 검사하고, ==로는 절대. 사용자가 검사할 수 있도록 내보내세요.

go
var ErrNotFound = errors.New("not found")
func Find(id int) (*User, error) {
    if id > 100 { return nil, ErrNotFound }
    return &User{}, nil
}
// Check with errors.Is
if errors.Is(err, ErrNotFound) { /* ... */ }

에러 처리 패턴

가능하면 에러를 즉시 처리하세요. Close 같은 지연된 작업의 경우 에러를 캡처하세요. 명명된 반환 값은 지연된 함수가 반환 값을 수정할 수 있게 합니다. 항상 Close 에러를 확인하세요.

go
// Immediate handling
if err := doSomething(); err != nil {
    return fmt.Errorf("operation failed: %w", err)
}
// Deferred error checking (e.g., Close)
func readFile() (err error) {
    f, e := os.Open("file.txt")
    if e != nil { return e }
    defer func() {
        if cerr := f.Close(); err == nil { err = cerr }
    }()
    // ...
}
29

일반적인 함정

고루틴 누수

고루틴은 영원히 차단될 때 누수됩니다. 항상 종료 경로를 제공하세요: context 취소, 채널 닫기 또는 버퍼링된 채널. 누수를 감지하려면 runtime.NumGoroutine()을 사용하세요.

go
// BUG: goroutine leaks if receiver stops
func send(ch chan int) {
    go func() { ch <- 1 }()  // Blocks forever
}
// FIX: use context or buffered channel
func send(ctx context.Context, ch chan int) {
    go func() {
        select {
        case ch <- 1:
        case <-ctx.Done():
        }
    }()
}

채널 닫기

수신자가 아닌 송신자만 채널을 닫아야 합니다. 닫기는 더 이상 값이 없음을 신호합니다. 닫힌 채널에서 수신하면 제로 값을 반환합니다. 닫힌 채널에 송신하면 패닉이 발생합니다.

go
// Only the sender should close a channel
// Closing from receiver causes panic
ch := make(chan int)
go func() {
    defer close(ch)  // Sender closes
    for i := 0; i < 5; i++ { ch <- i }
}()
for v := range ch { fmt.Println(v) }

루프 변수 캡처

Go 1.22 이전에는 루프 변수가 반복 간에 공유되었습니다. 캡처하는 고루틴은 최종 값을 봅니다. Go 1.22+는 반복당 새 변수를 만들어 이를 수정합니다. 이전 버전에서는 매개변수로 전달하세요.

go
// BUG (Go < 1.22): all goroutines see last value
for i := 0; i < 3; i++ {
    go func() { fmt.Println(i) }()  // Prints 3,3,3
}
// FIX: pass as parameter
for i := 0; i < 3; i++ {
    go func(i int) { fmt.Println(i) }(i)
}

맵 동시성

맵은 동시 사용에 안전하지 않습니다. 동시 읽기와 쓰기는 런타임 패닉을 일으킵니다. 명시적 잠금을 위해 sync.Mutex를 사용하거나 읽기 많은 동시 접근을 위해 sync.Map을 사용하세요.

go
// BUG: concurrent map writes panic
m := map[int]int{}
go func() { m[1] = 1 }()
go func() { m[2] = 2 }()
// FIX: use sync.Map or mutex
var mu sync.Mutex
mu.Lock(); m[1] = 1; mu.Unlock()

Nil 인터페이스

인터페이스에 래핑된 nil 포인터는 nil이 아닙니다. 값이 nil이어도 인터페이스는 타입을 가집니다. nil 타입화된 포인터가 아닌 nil을 직접 반환하세요. reflect로 확인하거나 명시적으로 nil을 반환하세요.

go
// BUG: nil check fails
type MyError struct{}
func (e *MyError) Error() string { return "err" }
func doSomething() error {
    var err *MyError = nil
    return err  // Non-nil interface!
}
// FIX: return nil explicitly
func doSomething() error {
    return nil
}

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.