Updated:

12 minute read

개요

panic은 Go에서 복구 불가능한 에러 상황을 처리하는 메커니즘입니다.

주요 특징:

  • 즉시 실행 중단: panic 발생 시 현재 함수 실행 중단
  • defer 실행: panic 발생 전 등록된 defer는 모두 실행됨
  • 스택 언와인딩: 호출 스택을 거슬러 올라가며 전파됨
  • 스택 트레이스: 자동으로 상세한 오류 정보 출력
  • recover로 복구: defer 안에서 recover()로 패닉 포착 가능
  • 프로그램 종료: recover하지 않으면 프로그램 종료
  • 예외적 사용: 일반적인 에러는 error로 처리 권장

panic vs error

1. error 사용 (권장)

package main

import (
    "fmt"
    "errors"
)

// ✅ 예상 가능한 에러는 error 반환
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

func main() {
    result, err := divide(10, 0)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    fmt.Println("Result:", result)
}

2. panic 사용 (예외적 상황)

// ❌ 일반적인 에러에는 사용하지 말 것
func badDivide(a, b float64) float64 {
    if b == 0 {
        panic("division by zero") // 과도함
    }
    return a / b
}

// ✅ panic이 적절한 경우
func mustLoadConfig(path string) Config {
    config, err := loadConfig(path)
    if err != nil {
        // 프로그램 시작 시 필수 설정 파일을 로드할 수 없으면
        // 계속 실행할 수 없으므로 panic
        panic(fmt.Sprintf("cannot load config: %v", err))
    }
    return config
}

type Config struct {
    Port int
}

func loadConfig(path string) (Config, error) {
    // 설정 로드 로직
    return Config{Port: 8080}, nil
}

panic 발생 상황

1. 명시적 panic 호출

func explicitPanic() {
    panic("something went wrong")
}

func main() {
    explicitPanic()
    // panic: something went wrong
}

2. 런타임 panic (자동 발생)

func runtimePanics() {
    // 1. nil 포인터 역참조
    var p *int
    fmt.Println(*p) // panic: runtime error: invalid memory address

    // 2. 인덱스 범위 초과
    arr := []int{1, 2, 3}
    fmt.Println(arr[10]) // panic: runtime error: index out of range

    // 3. 타입 단언 실패
    var i interface{} = "hello"
    num := i.(int) // panic: interface conversion

    // 4. nil 맵에 쓰기
    var m map[string]int
    m["key"] = 1 // panic: assignment to entry in nil map

    // 5. 닫힌 채널에 쓰기
    ch := make(chan int)
    close(ch)
    ch <- 1 // panic: send on closed channel

    // 6. 0으로 나누기 (정수)
    x := 10
    y := 0
    fmt.Println(x / y) // panic: runtime error: integer divide by zero
}

3. 표준 라이브러리 panic

import "regexp"

func libraryPanic() {
    // regexp.MustCompile은 잘못된 정규식에 panic
    re := regexp.MustCompile("[invalid(") // panic
    _ = re
}

panic 동작 과정

func demoFlow() {
    fmt.Println("1. Start")
    
    defer fmt.Println("5. Defer 1")
    
    fmt.Println("2. Before panic")
    
    defer fmt.Println("4. Defer 2")
    
    panic("3. Panic!")
    
    fmt.Println("Never executed") // 실행되지 않음
}

func main() {
    demoFlow()
}

// 출력:
// 1. Start
// 2. Before panic
// 4. Defer 2    ← LIFO 순서
// 5. Defer 1
// panic: 3. Panic!
// [스택 트레이스]

recover로 panic 복구

1. 기본 recover

func recoverExample() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered from panic:", r)
        }
    }()
    
    fmt.Println("About to panic")
    panic("something bad happened")
    fmt.Println("This won't print")
}

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

// 출력:
// About to panic
// Recovered from panic: something bad happened
// Program continues normally

2. recover는 defer 안에서만 작동

func wrongRecover() {
    // ❌ defer 밖에서 recover는 작동 안 함
    if r := recover(); r != nil {
        fmt.Println("Recovered:", r) // 실행 안됨
    }
    
    panic("error")
}

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

3. panic 타입 확인

type CustomError struct {
    Code    int
    Message string
}

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

func recoverWithType() {
    defer func() {
        if r := recover(); r != nil {
            switch v := r.(type) {
            case string:
                fmt.Println("String panic:", v)
            case CustomError:
                fmt.Printf("Custom error [%d]: %s\n", v.Code, v.Message)
            case error:
                fmt.Println("Error panic:", v)
            default:
                fmt.Printf("Unknown panic type: %T, value: %v\n", v, v)
            }
        }
    }()
    
    // 다양한 타입으로 panic 가능
    panic(CustomError{Code: 500, Message: "internal error"})
}

4. 스택 트레이스 포함 복구

import (
    "fmt"
    "runtime/debug"
)

func recoverWithStack() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Printf("Panic: %v\n", r)
            fmt.Printf("Stack trace:\n%s\n", debug.Stack())
        }
    }()
    
    causeDeepPanic()
}

func causeDeepPanic() {
    level1()
}

func level1() {
    level2()
}

func level2() {
    panic("deep error")
}

실전 활용 패턴

1. HTTP 핸들러 panic 복구

import "net/http"

func SafeHandler(handler http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                fmt.Printf("Panic in handler: %v\n", err)
                fmt.Printf("Stack: %s\n", debug.Stack())
                
                http.Error(w, "Internal Server Error", 
                    http.StatusInternalServerError)
            }
        }()
        
        handler(w, r)
    }
}

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

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

func main() {
    http.HandleFunc("/api", SafeHandler(riskyHandler))
    http.ListenAndServe(":8080", nil)
}

2. Goroutine panic 처리

func SafeGo(fn func()) {
    go func() {
        defer func() {
            if r := recover(); r != nil {
                fmt.Printf("Goroutine panic: %v\n", r)
                fmt.Printf("Stack: %s\n", debug.Stack())
            }
        }()
        
        fn()
    }()
}

func main() {
    // ❌ panic이 프로그램을 종료시킴
    // go func() {
    //     panic("goroutine error")
    // }()
    
    // ✅ panic이 복구됨
    SafeGo(func() {
        panic("goroutine error")
    })
    
    time.Sleep(100 * time.Millisecond)
    fmt.Println("Main continues")
}

3. 검증 함수 (Must 패턴)

import "regexp"

// Must 패턴: 에러를 panic으로 변환
func Must[T any](value T, err error) T {
    if err != nil {
        panic(err)
    }
    return value
}

func initializeApp() {
    // 초기화 시에만 사용 (런타임에는 사용 금지)
    config := Must(loadConfig("config.yaml"))
    db := Must(connectDB(config.DBUrl))
    
    fmt.Printf("Initialized with config: %+v, db: %v\n", config, db)
}

func loadConfig(path string) (Config, error) {
    return Config{Port: 8080, DBUrl: "postgres://..."}, nil
}

func connectDB(url string) (*DB, error) {
    return &DB{url: url}, nil
}

type DB struct {
    url string
}

// 표준 라이브러리 예제
func compileRegex() {
    // 프로그램 시작 시 한 번만
    emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
    _ = emailRegex
}

4. 불변 조건 검증 (assertion)

func assert(condition bool, message string) {
    if !condition {
        panic("assertion failed: " + message)
    }
}

func processData(data []int) {
    // 디버그 빌드에서만 사용
    assert(len(data) > 0, "data must not be empty")
    assert(data[0] >= 0, "first element must be non-negative")
    
    // 실제 처리
    result := data[0] * 2
    fmt.Println(result)
}

5. 리소스 정리와 panic

func processFile(filename string) (err error) {
    file, err := os.Open(filename)
    if err != nil {
        return err
    }
    defer file.Close()
    
    // panic 복구하여 error로 변환
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("panic occurred: %v", r)
        }
    }()
    
    // 패닉 발생 가능한 처리
    riskyProcessing(file)
    
    return nil
}

func riskyProcessing(file *os.File) {
    // 위험한 작업
}

6. panic 재발생 (re-panic)

func logAndRePanic() {
    defer func() {
        if r := recover(); r != nil {
            // 로그 기록
            fmt.Printf("Panic logged: %v\n", r)
            
            // 특정 조건에서 재발생
            if shouldRePanic(r) {
                panic(r) // 상위로 전파
            }
        }
    }()
    
    panic("critical error")
}

func shouldRePanic(r interface{}) bool {
    // 심각한 에러인지 판단
    if err, ok := r.(error); ok {
        return err.Error() == "critical error"
    }
    return false
}

7. Worker Pool panic 처리

type WorkerPool struct {
    workers int
    tasks   chan func()
}

func NewWorkerPool(workers int) *WorkerPool {
    pool := &WorkerPool{
        workers: workers,
        tasks:   make(chan func(), 100),
    }
    
    for i := 0; i < workers; i++ {
        go pool.worker(i)
    }
    
    return pool
}

func (p *WorkerPool) worker(id int) {
    for task := range p.tasks {
        func() {
            defer func() {
                if r := recover(); r != nil {
                    fmt.Printf("Worker %d panic: %v\n", id, r)
                    fmt.Printf("Stack: %s\n", debug.Stack())
                }
            }()
            
            task()
        }()
    }
}

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

func main() {
    pool := NewWorkerPool(5)
    
    pool.Submit(func() {
        panic("task error") // 워커는 계속 동작
    })
    
    pool.Submit(func() {
        fmt.Println("This task runs fine")
    })
    
    time.Sleep(100 * time.Millisecond)
}

panic과 goroutine

import "sync"

func panicInGoroutine() {
    var wg sync.WaitGroup
    
    // ❌ goroutine의 panic은 복구 안 됨
    wg.Add(1)
    go func() {
        defer wg.Done()
        panic("goroutine panic") // 프로그램 종료!
    }()
    
    wg.Wait()
}

func safePanicInGoroutine() {
    var wg sync.WaitGroup
    
    // ✅ 각 goroutine이 자체 복구
    wg.Add(1)
    go func() {
        defer wg.Done()
        defer func() {
            if r := recover(); r != nil {
                fmt.Println("Recovered in goroutine:", r)
            }
        }()
        
        panic("goroutine panic")
    }()
    
    wg.Wait()
    fmt.Println("All goroutines completed")
}

스택 트레이스 분석

func demonstrateStackTrace() {
    function1()
}

func function1() {
    function2()
}

func function2() {
    function3()
}

func function3() {
    panic("error in function3")
}

// 출력:
// panic: error in function3
//
// goroutine 1 [running]:
// main.function3(...)
//     /path/to/file.go:XX
// main.function2(...)
//     /path/to/file.go:XX
// main.function1(...)
//     /path/to/file.go:XX
// main.demonstrateStackTrace(...)
//     /path/to/file.go:XX
// main.main()
//     /path/to/file.go:XX

커스텀 스택 트레이스

import "runtime"

func customStackTrace() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Panic:", r)
            
            // 커스텀 스택 정보
            for i := 0; ; i++ {
                pc, file, line, ok := runtime.Caller(i)
                if !ok {
                    break
                }
                
                fn := runtime.FuncForPC(pc)
                fmt.Printf("%s:%d %s\n", file, line, fn.Name())
            }
        }
    }()
    
    panic("custom trace")
}

일반적인 실수

1. panic을 일반 에러 처리로 사용

// ❌ 나쁜 예: 예상 가능한 에러에 panic 사용
func badReadFile(filename string) []byte {
    data, err := os.ReadFile(filename)
    if err != nil {
        panic(err) // 파일이 없을 수 있는 것은 정상적 상황
    }
    return data
}

// ✅ 좋은 예: error 반환
func goodReadFile(filename string) ([]byte, error) {
    data, err := os.ReadFile(filename)
    if err != nil {
        return nil, err
    }
    return data, nil
}

2. recover를 defer 밖에서 호출

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

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

3. goroutine panic 무시

func mistake3() {
    // ❌ goroutine panic은 전체 프로그램 종료
    go func() {
        panic("boom") // 복구 안됨!
    }()
    
    time.Sleep(1 * time.Second)
}

func fixed3() {
    // ✅ 각 goroutine에서 복구
    go func() {
        defer func() {
            if r := recover(); r != nil {
                fmt.Println("Recovered:", r)
            }
        }()
        
        panic("boom")
    }()
    
    time.Sleep(1 * time.Second)
}

4. panic에 nil 전달

func mistake4() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered:", r)
        }
    }()
    
    // ❌ nil panic은 recover가 nil 반환
    panic(nil)
    // recover()가 nil을 반환하므로 if문 실행 안됨
}

func fixed4() {
    defer func() {
        r := recover()
        // ✅ panic 발생 여부를 별도로 추적
        if r != nil || panicOccurred {
            fmt.Println("Recovered:", r)
        }
    }()
    
    panicOccurred = true
    panic(nil)
}

var panicOccurred bool

5. defer 순서 오해

func mistake5() {
    panic("first")
    
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Won't execute")
        }
    }()
}

func fixed5() {
    // ✅ panic 전에 defer 등록
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered")
        }
    }()
    
    panic("error")
}

6. recover 반환값 미확인

func mistake6() {
    defer func() {
        recover() // ❌ 반환값 확인 안 함
        fmt.Println("Cleanup") // 항상 실행됨
    }()
    
    panic("error")
}

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

7. panic으로 제어 흐름 관리

// ❌ 나쁜 예: panic을 제어 흐름으로 사용
func badControlFlow() {
    defer func() {
        if r := recover(); r != nil {
            if r == "skip" {
                // panic으로 조건부 실행
            }
        }
    }()
    
    if someCondition {
        panic("skip")
    }
    
    // 정상 로직
}

var someCondition = true

// ✅ 좋은 예: 일반적인 제어 흐름 사용
func goodControlFlow() {
    if someCondition {
        return
    }
    
    // 정상 로직
}

고급 패턴

1. panic 체인

func chainedPanic() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("First recover:", r)
            
            // 새로운 panic 발생
            panic(fmt.Sprintf("wrapped: %v", r))
        }
    }()
    
    panic("original error")
}

func handleChainedPanic() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Final recover:", r)
            // wrapped: original error
        }
    }()
    
    chainedPanic()
}

2. 조건부 panic 복구

func conditionalRecover() {
    defer func() {
        if r := recover(); r != nil {
            // 특정 타입의 panic만 복구
            if err, ok := r.(RecoverableError); ok {
                fmt.Println("Recovered error:", err)
                return
            }
            
            // 다른 panic은 재발생
            panic(r)
        }
    }()
    
    panic(RecoverableError{Message: "temporary issue"})
}

type RecoverableError struct {
    Message string
}

func (e RecoverableError) Error() string {
    return e.Message
}

3. panic 전환 (error로 변환)

func panicToError() (err error) {
    defer func() {
        if r := recover(); r != nil {
            // panic을 error로 변환
            switch v := r.(type) {
            case error:
                err = v
            case string:
                err = fmt.Errorf("panic: %s", v)
            default:
                err = fmt.Errorf("panic: %v", v)
            }
        }
    }()
    
    riskyOperation()
    return nil
}

func riskyOperation() {
    panic("something went wrong")
}

4. 타임아웃과 panic

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

func main() {
    err := withTimeout(func() {
        time.Sleep(2 * time.Second)
        panic("delayed panic")
    }, 1*time.Second)
    
    fmt.Println(err) // timeout after 1s
}

5. 메트릭 수집

type PanicMetrics struct {
    mu          sync.Mutex
    panicCount  int
    panicTypes  map[string]int
}

func NewPanicMetrics() *PanicMetrics {
    return &PanicMetrics{
        panicTypes: make(map[string]int),
    }
}

func (pm *PanicMetrics) RecordPanic(r interface{}) {
    pm.mu.Lock()
    defer pm.mu.Unlock()
    
    pm.panicCount++
    
    panicType := fmt.Sprintf("%T", r)
    pm.panicTypes[panicType]++
}

func (pm *PanicMetrics) SafeCall(fn func()) {
    defer func() {
        if r := recover(); r != nil {
            pm.RecordPanic(r)
            fmt.Printf("Panic recorded: %v\n", r)
        }
    }()
    
    fn()
}

panic 사용 가이드라인

언제 panic을 사용해야 하나?

// ✅ 적절한 사용:

// 1. 프로그램 초기화 실패 (불가능한 계속 실행)
func init() {
    config := Must(loadConfig("config.yaml"))
    _ = config
}

// 2. 프로그래머 에러 (버그)
func processData(data []int) {
    if len(data) == 0 {
        panic("processData called with empty data - this is a bug")
    }
    // 처리
}

// 3. 불가능한 상황
func unreachable() {
    value := getSomeValue()
    switch value {
    case "a":
        // 처리
    case "b":
        // 처리
    default:
        panic("unreachable: unexpected value")
    }
}

func getSomeValue() string {
    return "a"
}

// ❌ 부적절한 사용:

// 1. 사용자 입력 검증
func badValidation(input string) {
    if input == "" {
        panic("empty input") // error 반환해야 함
    }
}

// 2. 외부 서비스 에러
func badAPICall() {
    resp, err := http.Get("https://api.example.com")
    if err != nil {
        panic(err) // error 처리해야 함
    }
    defer resp.Body.Close()
}

// 3. 일반적인 비즈니스 로직 에러
func badBusinessLogic(account Account) {
    if account.Balance < 0 {
        panic("negative balance") // error 반환해야 함
    }
}

type Account struct {
    Balance float64
}

디버깅 팁

1. 상세한 panic 메시지

func detailedPanic(user string, action string) {
    if !isAuthorized(user, action) {
        panic(fmt.Sprintf(
            "unauthorized: user=%s, action=%s, timestamp=%s",
            user, action, time.Now().Format(time.RFC3339),
        ))
    }
}

func isAuthorized(user, action string) bool {
    return user == "admin"
}

2. 컨텍스트 정보 포함

type PanicContext struct {
    Function  string
    File      string
    Line      int
    Message   string
    Timestamp time.Time
}

func contextualPanic(msg string) {
    pc, file, line, _ := runtime.Caller(1)
    fn := runtime.FuncForPC(pc)
    
    panic(PanicContext{
        Function:  fn.Name(),
        File:      file,
        Line:      line,
        Message:   msg,
        Timestamp: time.Now(),
    })
}

3. 조건부 panic (디버그 모드)

const DebugMode = true

func debugPanic(msg string) {
    if DebugMode {
        panic(msg)
    } else {
        fmt.Fprintf(os.Stderr, "Error: %s\n", msg)
    }
}

정리

  • panic: 복구 불가능한 에러 처리 메커니즘
  • 사용 원칙: 예상 가능한 에러는 error, 예외적 상황만 panic
  • defer 실행: panic 발생 시에도 defer는 LIFO 순서로 실행
  • recover: defer 안에서만 작동, panic 복구 가능
  • goroutine: 각 goroutine은 자체적으로 recover 필요
  • 스택 트레이스: 자동으로 상세한 디버그 정보 제공
  • 재발생: recover 후 조건부로 panic 재발생 가능
  • error 변환: panic을 error로 변환하여 반환 가능
  • Must 패턴: 초기화 시 에러를 panic으로 변환
  • 안티패턴: 제어 흐름, 일반 에러, 사용자 입력에 사용 금지
  • 디버깅: 상세한 메시지, 컨텍스트 정보 포함 권장
  • 복구 전략: HTTP 핸들러, Worker Pool 등에서 적절히 복구