[Go] panic
개요
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 등에서 적절히 복구