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이 발생해도 실행됨