Updated:

14 minute read

개요

recover는 panic 상태를 복구하고 정상 실행을 재개할 수 있게 하는 내장 함수입니다.

주요 특징:

  • panic 복구: 발생한 panic을 포착하여 프로그램 종료 방지
  • defer 내에서만 작동: defer 블록 안에서 호출해야만 유효
  • panic 값 반환: panic()에 전달된 값을 반환
  • nil 반환: panic이 없으면 nil 반환
  • 실행 재개: panic이 발생한 함수는 종료되지만 호출자는 계속 실행
  • 스택 언와인딩 중단: recover 시점에서 스택 전파 중단

recover 기본 동작

1. 기본 사용법

package main

import "fmt"

func basicRecover() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered from:", r)
        }
    }()
    
    fmt.Println("Before panic")
    panic("something went wrong")
    fmt.Println("After panic - never executed")
}

func main() {
    basicRecover()
    fmt.Println("Program continues normally")
}

// 출력:
// Before panic
// Recovered from: something went wrong
// Program continues normally

2. recover는 defer 내에서만 작동

func wrongRecover1() {
    // ❌ defer 밖에서는 작동 안 함
    if r := recover(); r != nil {
        fmt.Println("Won't work:", r)
    }
    
    panic("error")
}

func wrongRecover2() {
    defer recover() // ❌ 반환값을 확인하지 않음
    
    panic("error")
}

func correctRecover() {
    // ✅ defer 안에서 반환값 확인
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered:", r)
        }
    }()
    
    panic("error")
}

3. recover 반환값

func recoverReturnValues() {
    defer func() {
        r := recover()
        
        if r == nil {
            fmt.Println("No panic occurred")
            return
        }
        
        // panic 값의 타입 확인
        fmt.Printf("Panic value: %v (type: %T)\n", r, r)
    }()
    
    // 다양한 타입으로 panic 가능
    panic(42)              // int
    // panic("error")      // string
    // panic(fmt.Errorf("error")) // error
    // panic(CustomError{})       // custom type
}

type CustomError struct{}

func (CustomError) Error() string {
    return "custom error"
}

recover와 함수 실행 흐름

1. panic 발생 함수는 종료됨

func panicFunction() {
    defer fmt.Println("panicFunction defer")
    
    fmt.Println("Before panic")
    panic("error")
    fmt.Println("After panic - never executed")
}

func caller() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered in caller:", r)
        }
    }()
    
    fmt.Println("Before calling panicFunction")
    panicFunction()
    fmt.Println("After calling panicFunction - never executed")
}

func main() {
    caller()
    fmt.Println("main continues")
}

// 출력:
// Before calling panicFunction
// Before panic
// panicFunction defer
// Recovered in caller: error
// main continues

2. 여러 단계 호출 스택

func level3() {
    fmt.Println("Level 3: before panic")
    panic("error at level 3")
    fmt.Println("Level 3: after panic - never executed")
}

func level2() {
    defer fmt.Println("Level 2: defer")
    
    fmt.Println("Level 2: before level3")
    level3()
    fmt.Println("Level 2: after level3 - never executed")
}

func level1() {
    defer fmt.Println("Level 1: defer")
    
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Level 1: recovered -", r)
        }
    }()
    
    fmt.Println("Level 1: before level2")
    level2()
    fmt.Println("Level 1: after level2 - never executed")
}

func main() {
    level1()
    fmt.Println("Main continues")
}

// 출력:
// Level 1: before level2
// Level 2: before level3
// Level 3: before panic
// Level 2: defer
// Level 1: recovered - error at level 3
// Level 1: defer
// Main continues

recover의 범위

1. 직접 호출한 함수의 panic만 복구

func indirectPanic() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered:", r)
        }
    }()
    
    // ❌ 중첩 함수의 panic은 복구 안 됨
    func() {
        panic("nested panic")
    }()
}

func directPanic() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered:", r)
        }
    }()
    
    // ✅ 같은 함수의 panic 복구
    panic("direct panic")
}

2. goroutine 경계를 넘을 수 없음

func goroutinePanic() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Main goroutine recovered:", r)
        }
    }()
    
    // ❌ 다른 goroutine의 panic은 복구 안 됨
    go func() {
        panic("goroutine panic") // 프로그램 종료!
    }()
    
    time.Sleep(100 * time.Millisecond)
}

func safeGoroutine() {
    // ✅ 각 goroutine이 자체 복구
    go func() {
        defer func() {
            if r := recover(); r != nil {
                fmt.Println("Goroutine recovered:", r)
            }
        }()
        
        panic("goroutine panic")
    }()
    
    time.Sleep(100 * time.Millisecond)
}

실전 활용 패턴

1. 안전한 함수 래퍼

type SafeResult struct {
    Value interface{}
    Error error
}

func SafeCall(fn func() interface{}) SafeResult {
    var result interface{}
    var err error
    
    func() {
        defer func() {
            if r := recover(); r != nil {
                err = fmt.Errorf("panic: %v", r)
            }
        }()
        
        result = fn()
    }()
    
    return SafeResult{Value: result, Error: err}
}

func main() {
    // 성공 케이스
    res1 := SafeCall(func() interface{} {
        return "success"
    })
    fmt.Printf("Result: %v, Error: %v\n", res1.Value, res1.Error)
    
    // panic 케이스
    res2 := SafeCall(func() interface{} {
        panic("something went wrong")
    })
    fmt.Printf("Result: %v, Error: %v\n", res2.Value, res2.Error)
}

2. HTTP 미들웨어

import (
    "net/http"
    "runtime/debug"
)

func RecoveryMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                // 로그 기록
                fmt.Printf("Panic: %v\n", err)
                fmt.Printf("Stack trace:\n%s\n", debug.Stack())
                
                // 클라이언트에 500 응답
                http.Error(w, "Internal Server Error", 
                    http.StatusInternalServerError)
            }
        }()
        
        next.ServeHTTP(w, r)
    })
}

func riskyHandler(w http.ResponseWriter, r *http.Request) {
    // panic 발생 가능한 코드
    data := processRequest(r)
    w.Write([]byte(data))
}

func processRequest(r *http.Request) string {
    return "processed"
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/api", riskyHandler)
    
    // 미들웨어 적용
    handler := RecoveryMiddleware(mux)
    
    http.ListenAndServe(":8080", handler)
}

3. 워커 풀 복구

import "sync"

type Task func() error

type WorkerPool struct {
    workers   int
    tasks     chan Task
    wg        sync.WaitGroup
    panicChan chan interface{}
}

func NewWorkerPool(workers int) *WorkerPool {
    return &WorkerPool{
        workers:   workers,
        tasks:     make(chan Task, 100),
        panicChan: make(chan interface{}, workers),
    }
}

func (p *WorkerPool) Start() {
    for i := 0; i < p.workers; i++ {
        p.wg.Add(1)
        go p.worker(i)
    }
}

func (p *WorkerPool) worker(id int) {
    defer p.wg.Done()
    
    for task := range p.tasks {
        func() {
            defer func() {
                if r := recover(); r != nil {
                    fmt.Printf("Worker %d recovered from panic: %v\n", id, r)
                    fmt.Printf("Stack: %s\n", debug.Stack())
                    
                    // panic 정보 수집
                    select {
                    case p.panicChan <- r:
                    default:
                    }
                }
            }()
            
            if err := task(); err != nil {
                fmt.Printf("Worker %d task error: %v\n", id, err)
            }
        }()
    }
}

func (p *WorkerPool) Submit(task Task) {
    p.tasks <- task
}

func (p *WorkerPool) Stop() {
    close(p.tasks)
    p.wg.Wait()
    close(p.panicChan)
}

func main() {
    pool := NewWorkerPool(3)
    pool.Start()
    
    // 정상 작업
    pool.Submit(func() error {
        fmt.Println("Task 1 completed")
        return nil
    })
    
    // panic 발생 작업
    pool.Submit(func() error {
        panic("task panic")
    })
    
    // 정상 작업 (워커는 계속 동작)
    pool.Submit(func() error {
        fmt.Println("Task 3 completed")
        return nil
    })
    
    pool.Stop()
    
    // 수집된 panic 확인
    for p := range pool.panicChan {
        fmt.Println("Collected panic:", p)
    }
}

4. 타입별 복구 전략

type ValidationError struct {
    Field   string
    Message string
}

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

type SystemError struct {
    Code    int
    Message string
}

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

func handlePanicByType() {
    defer func() {
        if r := recover(); r != nil {
            switch err := r.(type) {
            case ValidationError:
                fmt.Printf("Validation failed: %s\n", err.Message)
                // 사용자에게 친절한 메시지
                
            case SystemError:
                fmt.Printf("System error occurred: %d\n", err.Code)
                // 관리자에게 알림
                
            case error:
                fmt.Printf("General error: %v\n", err)
                // 일반 에러 처리
                
            case string:
                fmt.Printf("String panic: %s\n", err)
                // 문자열 panic 처리
                
            default:
                fmt.Printf("Unknown panic type: %T, value: %v\n", err, err)
                // 알 수 없는 타입 처리
            }
        }
    }()
    
    // 다양한 타입으로 panic
    panic(ValidationError{Field: "email", Message: "invalid format"})
}

5. 조건부 재발생

func conditionalRePanic() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Panic caught:", r)
            
            // 특정 조건에서 재발생
            if isCritical(r) {
                fmt.Println("Re-panicking critical error")
                panic(r) // 상위로 전파
            }
            
            fmt.Println("Panic handled, continuing")
        }
    }()
    
    panic("critical: database connection lost")
}

func isCritical(r interface{}) bool {
    if msg, ok := r.(string); ok {
        return len(msg) > 8 && msg[:8] == "critical"
    }
    return false
}

func outerFunction() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Outer recovered:", r)
        }
    }()
    
    conditionalRePanic()
    fmt.Println("After conditionalRePanic")
}

6. 리소스 정리 보장

func processWithResources() error {
    resource1 := acquireResource1()
    defer resource1.Release()
    
    resource2 := acquireResource2()
    defer resource2.Release()
    
    defer func() {
        if r := recover(); r != nil {
            fmt.Printf("Panic during processing: %v\n", r)
            // 리소스는 defer에 의해 자동으로 정리됨
        }
    }()
    
    // panic 발생 가능한 작업
    riskyOperation()
    
    return nil
}

type Resource struct {
    name string
}

func (r *Resource) Release() {
    fmt.Printf("Releasing %s\n", r.name)
}

func acquireResource1() *Resource {
    return &Resource{name: "Resource1"}
}

func acquireResource2() *Resource {
    return &Resource{name: "Resource2"}
}

func riskyOperation() {
    // 작업 수행
}

7. 트랜잭션 롤백

import "database/sql"

func executeTransaction(db *sql.DB) (err error) {
    tx, err := db.Begin()
    if err != nil {
        return err
    }
    
    defer func() {
        if r := recover(); r != nil {
            tx.Rollback()
            err = fmt.Errorf("panic during transaction: %v", r)
            return
        }
        
        if err != nil {
            tx.Rollback()
        } else {
            err = tx.Commit()
        }
    }()
    
    // 트랜잭션 작업 (panic 발생 가능)
    if err := doWork1(tx); err != nil {
        return err
    }
    
    if err := doWork2(tx); err != nil {
        return err
    }
    
    return nil
}

func doWork1(tx *sql.Tx) error {
    return nil
}

func doWork2(tx *sql.Tx) error {
    return nil
}

8. 함수 체인 복구

type Result struct {
    Value interface{}
    Err   error
}

func Chain(fns ...func() interface{}) Result {
    var result interface{}
    var err error
    
    for i, fn := range fns {
        func() {
            defer func() {
                if r := recover(); r != nil {
                    err = fmt.Errorf("panic in step %d: %v", i+1, r)
                }
            }()
            
            if err != nil {
                return // 이전 단계 실패 시 건너뛰기
            }
            
            result = fn()
        }()
        
        if err != nil {
            break
        }
    }
    
    return Result{Value: result, Err: err}
}

func main() {
    res := Chain(
        func() interface{} {
            fmt.Println("Step 1")
            return 1
        },
        func() interface{} {
            fmt.Println("Step 2")
            panic("error in step 2")
        },
        func() interface{} {
            fmt.Println("Step 3 - not executed")
            return 3
        },
    )
    
    if res.Err != nil {
        fmt.Println("Chain failed:", res.Err)
    } else {
        fmt.Println("Chain succeeded:", res.Value)
    }
}

9. 메트릭 및 로깅

import (
    "log"
    "time"
)

type PanicLogger struct {
    logger *log.Logger
}

func NewPanicLogger() *PanicLogger {
    return &PanicLogger{
        logger: log.New(os.Stderr, "[PANIC] ", log.LstdFlags|log.Lshortfile),
    }
}

func (pl *PanicLogger) Recover() {
    if r := recover(); r != nil {
        pl.logger.Printf("Panic recovered: %v", r)
        pl.logger.Printf("Stack trace:\n%s", debug.Stack())
        
        // 메트릭 전송
        sendMetric("panic.count", 1)
        
        // 알림 발송
        sendAlert(fmt.Sprintf("Panic occurred: %v", r))
    }
}

func sendMetric(name string, value int) {
    fmt.Printf("Metric: %s = %d\n", name, value)
}

func sendAlert(message string) {
    fmt.Printf("Alert: %s\n", message)
}

func monitoredFunction() {
    defer NewPanicLogger().Recover()
    
    // 작업 수행
    panic("unexpected error")
}

10. 타임아웃과 함께 사용

func executeWithTimeout(fn func(), timeout time.Duration) error {
    done := make(chan error, 1)
    
    go func() {
        defer func() {
            if r := recover(); r != nil {
                done <- fmt.Errorf("panic: %v", r)
            }
        }()
        
        fn()
        done <- nil
    }()
    
    select {
    case err := <-done:
        return err
    case <-time.After(timeout):
        return fmt.Errorf("timeout after %v", timeout)
    }
}

func main() {
    // 정상 실행
    err1 := executeWithTimeout(func() {
        time.Sleep(100 * time.Millisecond)
        fmt.Println("Task completed")
    }, 1*time.Second)
    fmt.Println("Result 1:", err1)
    
    // 타임아웃
    err2 := executeWithTimeout(func() {
        time.Sleep(2 * time.Second)
    }, 500*time.Millisecond)
    fmt.Println("Result 2:", err2)
    
    // panic
    err3 := executeWithTimeout(func() {
        panic("task error")
    }, 1*time.Second)
    fmt.Println("Result 3:", err3)
}

고급 패턴

1. 다중 recover 계층

func multiLayerRecover() {
    // 최상위 복구
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Top level recovery:", r)
        }
    }()
    
    // 중간 계층
    func() {
        defer func() {
            if r := recover(); r != nil {
                fmt.Println("Middle level recovery:", r)
                
                // 조건부 재발생
                if shouldPropagate(r) {
                    panic(r)
                }
            }
        }()
        
        // 하위 계층
        func() {
            panic("error")
        }()
    }()
}

func shouldPropagate(r interface{}) bool {
    // 특정 조건 확인
    return false
}

2. recover와 Named Return

func recoverWithNamedReturn() (result int, err error) {
    defer func() {
        if r := recover(); r != nil {
            result = 0
            err = fmt.Errorf("recovered: %v", r)
        }
    }()
    
    // panic 발생
    panic("calculation error")
    
    return 42, nil
}

func main() {
    result, err := recoverWithNamedReturn()
    fmt.Printf("Result: %d, Error: %v\n", result, err)
    // Result: 0, Error: recovered: calculation error
}

3. panic 체인 추적

type PanicChain struct {
    panics []interface{}
}

func (pc *PanicChain) Recover() {
    if r := recover(); r != nil {
        pc.panics = append(pc.panics, r)
        fmt.Printf("Panic chain: %v\n", pc.panics)
    }
}

func chainedPanics() {
    chain := &PanicChain{}
    
    defer chain.Recover()
    
    func() {
        defer chain.Recover()
        
        func() {
            defer chain.Recover()
            panic("level 3")
        }()
        
        panic("level 2")
    }()
    
    panic("level 1")
}

4. 컨텍스트 기반 복구

import "context"

type contextKey string

const panicHandlerKey contextKey = "panicHandler"

type PanicHandler func(interface{})

func WithPanicHandler(ctx context.Context, handler PanicHandler) context.Context {
    return context.WithValue(ctx, panicHandlerKey, handler)
}

func recoverWithContext(ctx context.Context) {
    if r := recover(); r != nil {
        if handler, ok := ctx.Value(panicHandlerKey).(PanicHandler); ok {
            handler(r)
        } else {
            fmt.Println("Default recovery:", r)
        }
    }
}

func processWithContext(ctx context.Context) {
    defer recoverWithContext(ctx)
    
    panic("context-aware panic")
}

func main() {
    ctx := WithPanicHandler(context.Background(), func(r interface{}) {
        fmt.Println("Custom handler:", r)
    })
    
    processWithContext(ctx)
}

5. 재시도 메커니즘

func RetryWithRecover(fn func() error, maxRetries int) error {
    var lastErr error
    
    for i := 0; i < maxRetries; i++ {
        func() {
            defer func() {
                if r := recover(); r != nil {
                    lastErr = fmt.Errorf("panic on attempt %d: %v", i+1, r)
                }
            }()
            
            lastErr = fn()
        }()
        
        if lastErr == nil {
            return nil
        }
        
        fmt.Printf("Attempt %d failed: %v\n", i+1, lastErr)
        time.Sleep(time.Duration(i+1) * 100 * time.Millisecond)
    }
    
    return fmt.Errorf("all retries failed: %v", lastErr)
}

func main() {
    attempts := 0
    err := RetryWithRecover(func() error {
        attempts++
        if attempts < 3 {
            panic(fmt.Sprintf("attempt %d failed", attempts))
        }
        fmt.Println("Success on attempt", attempts)
        return nil
    }, 5)
    
    if err != nil {
        fmt.Println("Final error:", err)
    }
}

일반적인 실수

1. defer 밖에서 recover 호출

func mistake1() {
    // ❌ 작동 안 함
    if r := recover(); r != nil {
        fmt.Println("Won't work")
    }
    
    panic("error")
}

func fixed1() {
    // ✅ defer 안에서 호출
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Works correctly")
        }
    }()
    
    panic("error")
}

2. recover 반환값 무시

func mistake2() {
    defer func() {
        recover() // ❌ 반환값 확인 안 함
        fmt.Println("Always executes")
    }()
    
    panic("error")
}

func fixed2() {
    defer func() {
        if r := recover(); r != nil { // ✅ 반환값 확인
            fmt.Println("Panic occurred:", r)
        }
    }()
    
    panic("error")
}

3. 중첩 함수에서 recover

func mistake3() {
    defer func() {
        // ❌ 중첩 함수의 panic은 복구 안 됨
        if r := recover(); r != nil {
            fmt.Println("Won't catch nested panic")
        }
    }()
    
    go func() {
        panic("nested panic") // 다른 goroutine이므로 복구 안됨
    }()
    
    time.Sleep(100 * time.Millisecond)
}

func fixed3() {
    go func() {
        // ✅ 각 goroutine이 자체 복구
        defer func() {
            if r := recover(); r != nil {
                fmt.Println("Caught in goroutine:", r)
            }
        }()
        
        panic("nested panic")
    }()
    
    time.Sleep(100 * time.Millisecond)
}

4. nil panic 처리

func mistake4() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered:", r)
        }
    }()
    
    // ❌ panic(nil)은 recover가 nil 반환
    panic(nil)
    // if문이 실행되지 않음
}

func fixed4() {
    panicOccurred := false
    defer func() {
        r := recover()
        if panicOccurred || r != nil {
            fmt.Printf("Recovered (r=%v, occurred=%v)\n", r, panicOccurred)
        }
    }()
    
    panicOccurred = true
    panic(nil)
}

5. 에러와 panic 혼동

func mistake5() error {
    defer func() {
        if r := recover(); r != nil {
            // ❌ 정상 에러를 panic으로 처리
            return // 컴파일 에러: defer는 값 반환 불가
        }
    }()
    
    return fmt.Errorf("normal error")
}

func fixed5() (err error) {
    defer func() {
        if r := recover(); r != nil {
            // ✅ Named return으로 에러 설정
            err = fmt.Errorf("panic: %v", r)
        }
    }()
    
    panic("unexpected error")
}

6. recover 후 상태 복원 실패

var globalState int = 0

func mistake6() {
    globalState = 1
    
    defer func() {
        if r := recover(); r != nil {
            // ❌ 상태 복원 안 함
            fmt.Println("Recovered")
        }
    }()
    
    globalState = 2
    panic("error")
    // globalState는 2로 남음
}

func fixed6() {
    oldState := globalState
    globalState = 1
    
    defer func() {
        if r := recover(); r != nil {
            // ✅ 상태 복원
            globalState = oldState
            fmt.Println("Recovered and restored state")
        }
    }()
    
    globalState = 2
    panic("error")
    // globalState가 원래 값으로 복원됨
}

베스트 프랙티스

1. 항상 panic 여부 확인

func bestPractice1() {
    defer func() {
        // ✅ nil 체크 필수
        if r := recover(); r != nil {
            handlePanic(r)
        }
    }()
    
    riskyOperation()
}

func handlePanic(r interface{}) {
    fmt.Printf("Panic handled: %v\n", r)
}

2. 스택 트레이스 로깅

func bestPractice2() {
    defer func() {
        if r := recover(); r != nil {
            // ✅ 스택 정보 로깅
            fmt.Printf("Panic: %v\n", r)
            fmt.Printf("Stack:\n%s\n", debug.Stack())
        }
    }()
    
    riskyOperation()
}

3. 타입 안전 복구

func bestPractice3() {
    defer func() {
        if r := recover(); r != nil {
            // ✅ 타입별로 적절히 처리
            switch v := r.(type) {
            case error:
                fmt.Println("Error panic:", v.Error())
            case string:
                fmt.Println("String panic:", v)
            default:
                fmt.Printf("Unknown panic: %v (type: %T)\n", v, v)
            }
        }
    }()
    
    riskyOperation()
}

4. 리소스 정리와 함께

func bestPractice4() {
    resource := acquireResource()
    defer resource.Close()
    
    defer func() {
        if r := recover(); r != nil {
            // ✅ panic 처리
            fmt.Println("Panic after resource cleanup:", r)
        }
    }()
    
    riskyOperation()
}

type ResourceHandle struct{}

func (r *ResourceHandle) Close() {
    fmt.Println("Resource closed")
}

func acquireResource() *ResourceHandle {
    return &ResourceHandle{}
}

5. 조건부 재발생

func bestPractice5() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("First recovery:", r)
            
            // ✅ 심각한 에러는 재발생
            if isCriticalError(r) {
                panic(r)
            }
        }
    }()
    
    riskyOperation()
}

func isCriticalError(r interface{}) bool {
    return false
}

정리

  • recover: panic을 포착하여 프로그램 종료 방지
  • defer 필수: defer 블록 안에서만 작동
  • 반환값 확인: nil이 아닌 경우만 panic 발생
  • 함수 종료: panic 발생 함수는 종료되지만 호출자는 계속
  • goroutine 독립: 각 goroutine은 자체적으로 recover 필요
  • 타입 확인: type switch로 panic 값 타입 확인
  • 스택 트레이스: debug.Stack()으로 상세 정보 수집
  • 리소스 정리: defer로 정리 보장, recover로 안전성 확보
  • 재발생 가능: 조건부로 panic 재발생하여 전파
  • Named return: recover에서 반환값 수정 가능
  • 메트릭/로깅: panic 발생 추적 및 알림
  • 안티패턴: defer 밖 호출, 반환값 무시, goroutine 무시
  • 사용 지침: 예외적 상황 복구, 일반 에러는 error 사용