[Go] recover
개요
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 사용