본문으로 건너뛰기

panic과 recover

panic은 프로그램이 정상적으로 계속 실행될 수 없는 상황에서 발생합니다. Go에서는 예외(exception) 대신 error 반환을 권장하지만, 복구 불가능한 상황이나 프로그래밍 오류에는 panic을 사용합니다. recover는 defer 안에서만 사용 가능하며, 패닉을 안전하게 처리합니다.

panic 기본​

package main

import "fmt"

func riskyOperation(n int) {
if n == 0 {
panic("n cannot be zero") // 문자열로 패닉
}
if n < 0 {
panic(fmt.Sprintf("n must be positive, got %d", n)) // 포맷팅
}
fmt.Println("결과:", 100/n)
}

func accessSlice(s []int, i int) int {
// 인덱스 범위 초과 — Go 런타임이 자동으로 panic 발생
return s[i]
}

func main() {
// 정상 동작
riskyOperation(5) // 결과: 20

// 런타임 패닉 예제 (직접 실행 시 아래 panic 주석 해제)
// s := []int{1, 2, 3}
// fmt.Println(accessSlice(s, 10)) // index out of range [10] with length 3

// panic 발생 — 이 아래 코드는 실행되지 않음
// riskyOperation(0) // goroutine 1 [running]: main.riskyOperation(...)

fmt.Println("프로그램 정상 종료")
}

recover로 패닉 복구​

recover는 defer 함수 안에서만 패닉을 잡을 수 있습니다.

package main

import (
"fmt"
"runtime/debug"
)

// 패닉을 에러로 변환하는 래퍼
func safeCall(fn func()) (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic recovered: %v", r)
}
}()
fn()
return nil
}

// 스택 트레이스 포함 버전
func safeCallWithStack(fn func()) (err error) {
defer func() {
if r := recover(); r != nil {
stack := debug.Stack()
err = fmt.Errorf("panic recovered: %v\nstack:\n%s", r, stack)
}
}()
fn()
return nil
}

func dangerousFunc() {
panic("something went terribly wrong")
}

func nilPointerFunc() {
var p *int
_ = *p // nil 포인터 역참조
}

func indexOutOfRangeFunc() {
s := []int{1, 2, 3}
_ = s[10] // 인덱스 범위 초과
}

func main() {
// safeCall로 패닉 복구
testCases := []struct {
name string
fn func()
}{
{"dangerousFunc", dangerousFunc},
{"nilPointerFunc", nilPointerFunc},
{"indexOutOfRangeFunc", indexOutOfRangeFunc},
{"정상 함수", func() { fmt.Println(" → 정상 실행") }},
}

for _, tc := range testCases {
fmt.Printf("=== %s ===\n", tc.name)
if err := safeCall(tc.fn); err != nil {
fmt.Printf("에러: %v\n", err)
} else {
fmt.Println("성공")
}
}
}

라이브러리에서의 panic 정책​

라이브러리 개발 시 외부로 panic이 전파되지 않도록 내부에서 반드시 복구해야 합니다.

package main

import (
"errors"
"fmt"
)

// 라이브러리 내부에서 panic을 error로 변환하는 패턴
// (실제 라이브러리 코드 스타일)

// JSONParser — 내부적으로 panic을 사용하지만 외부에는 error로 변환
type JSONParser struct{}

// 내부용 — panic 사용 (성능/편의를 위해)
func (p *JSONParser) parseValue(data string, pos int) (any, int) {
if pos >= len(data) {
panic(fmt.Sprintf("unexpected end of input at position %d", pos))
}
switch data[pos] {
case '"':
// 문자열 파싱
end := pos + 1
for end < len(data) && data[end] != '"' {
end++
}
if end >= len(data) {
panic("unterminated string")
}
return data[pos+1 : end], end + 1
case 't':
if pos+4 <= len(data) && data[pos:pos+4] == "true" {
return true, pos + 4
}
panic("invalid token")
case 'f':
if pos+5 <= len(data) && data[pos:pos+5] == "false" {
return false, pos + 5
}
panic("invalid token")
default:
panic(fmt.Sprintf("unexpected character %q at position %d", data[pos], pos))
}
}

// 공개 API — panic을 error로 변환 (사용자는 error만 봄)
func (p *JSONParser) Parse(data string) (result any, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("json parse error: %v", r)
result = nil
}
}()

if data == "" {
return nil, errors.New("empty input")
}

value, _ := p.parseValue(data, 0)
return value, nil
}

// 계산기 — 내부 panic, 공개 error
type Calculator struct{}

func (c *Calculator) mustDivide(a, b float64) float64 {
if b == 0 {
panic("division by zero")
}
return a / b
}

func (c *Calculator) complexCalc(a, b, c2 float64) float64 {
// 내부 연산 체인 — panic 사용으로 에러 체크 간소화
step1 := c.mustDivide(a, b)
step2 := c.mustDivide(step1, c2)
return step2
}

// 공개 API
func (c *Calculator) Calculate(a, b, c2 float64) (result float64, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("calculation failed: %v", r)
}
}()
result = c.complexCalc(a, b, c2)
return result, nil
}

func main() {
// JSONParser 테스트
parser := &JSONParser{}
inputs := []string{
`"hello"`,
`true`,
`false`,
`"unterminated`,
`xyz`,
``,
}

fmt.Println("=== JSON Parser ===")
for _, input := range inputs {
val, err := parser.Parse(input)
if err != nil {
fmt.Printf("파싱 실패 %q: %v\n", input, err)
} else {
fmt.Printf("파싱 성공 %q → %v (%T)\n", input, val, val)
}
}

// Calculator 테스트
fmt.Println("\n=== Calculator ===")
calc := &Calculator{}
cases := [][3]float64{
{100, 5, 4}, // 100 / 5 / 4 = 5
{100, 0, 4}, // 0으로 나누기
{100, 5, 0}, // 0으로 나누기
}
for _, c := range cases {
result, err := calc.Calculate(c[0], c[1], c[2])
if err != nil {
fmt.Printf("계산 실패 (%.0f, %.0f, %.0f): %v\n", c[0], c[1], c[2], err)
} else {
fmt.Printf("계산 결과 (%.0f, %.0f, %.0f) = %.2f\n", c[0], c[1], c[2], result)
}
}
}

panic을 사용하는 적절한 경우​

package main

import "fmt"

// 적절한 panic 사용 사례 1: 프로그래밍 오류 — 잘못된 사용 방법 방지
func NewPositiveInt(n int) int {
if n <= 0 {
// 프로그래머가 API를 잘못 사용한 경우 — panic이 적절
panic(fmt.Sprintf("NewPositiveInt: n must be positive, got %d", n))
}
return n
}

// 적절한 panic 사용 사례 2: 초기화 실패 — 서버가 시작도 못하는 경우
func mustLoadConfig(path string) map[string]string {
if path == "" {
panic("config path cannot be empty — server cannot start")
}
// 실제로는 파일 로드
return map[string]string{"port": "8080", "host": "localhost"}
}

// Must 패턴 — 에러를 panic으로 변환, 초기화 시에만 사용
func Must[T any](val T, err error) T {
if err != nil {
panic(err)
}
return val
}

// 부적절한 panic 사용 사례 (에러 반환이 맞음)
// Bad:
func badOpenFile(path string) string {
if path == "" {
panic("path required") // Bad! 일반적인 에러 상황
}
return "content"
}

// Good:
func goodOpenFile(path string) (string, error) {
if path == "" {
return "", fmt.Errorf("path is required") // Good! error 반환
}
return "content", nil
}

func main() {
// 올바른 사용
n := NewPositiveInt(42)
fmt.Println("양수:", n)

config := mustLoadConfig("config.yaml")
fmt.Println("설정:", config)

// Must 패턴
// result := Must(strconv.Atoi("123")) — 초기화 시 사용
// result := Must(strconv.Atoi("abc")) — panic!

// 잘못된 사용 방지 (아래 주석 해제 시 panic)
// NewPositiveInt(-1) // panic: NewPositiveInt: n must be positive, got -1

fmt.Println("\npanic vs error 판단 기준:")
fmt.Println("panic → 프로그래밍 오류, 초기화 실패, 복구 불가 상황")
fmt.Println("error → 예측 가능한 실패 (파일 없음, 네트워크 실패, 검증 실패)")
}

defer와 panic의 실행 순서​

package main

import "fmt"

func demonstrateOrder() {
defer fmt.Println("defer 1 — 가장 마지막에 실행")
defer fmt.Println("defer 2")
defer fmt.Println("defer 3 — 가장 먼저 실행 (LIFO)")

fmt.Println("함수 본문 실행")
// defer는 스택(LIFO)으로 쌓임
}

func panicWithDefer() {
defer fmt.Println("defer A — panic 이후에도 실행됨!")
defer fmt.Println("defer B — panic 이후에도 실행됨!")

fmt.Println("panic 발생 전")
panic("테스트 패닉")
fmt.Println("이 줄은 실행되지 않음") // 실행 안 됨
}

func recoverExample() (result string) {
defer func() {
if r := recover(); r != nil {
fmt.Println("패닉 복구:", r)
result = "recovered" // 반환값 수정 가능
}
}()

panicFunc := func() {
panic("inner panic")
}
panicFunc()
return "normal" // 실행 안 됨
}

func main() {
fmt.Println("=== defer 실행 순서 ===")
demonstrateOrder()

fmt.Println("\n=== panic + defer ===")
// panicWithDefer() // 이 함수는 recover 없으므로 프로그램 종료됨

fmt.Println("\n=== recover 예제 ===")
result := recoverExample()
fmt.Println("결과:", result)
fmt.Println("프로그램 계속 실행 중...")
}

핵심 정리

  • panic은 예외가 아닌 비정상 종료 신호— 일반 에러에는 사용하지 말 것
  • recover는 반드시 defer 함수 안에서만 작동
  • 라이브러리는 내부 panic을 error로 변환해서 API 사용자에게 노출하지 않는다
  • panic이 적합한 경우: 프로그래밍 오류, 초기화 실패, 복구 불가능한 상황
  • Must 패턴은 초기화 단계에서만 사용 (런타임 중에는 금지)
  • defer는 LIFO(Last In First Out) 순서로 실행되며, panic이 발생해도 실행됨