Menu

Go panic과 recover: 런타임 패닉과 패닉을 써야 할 때

패닉은 정상 실행을 멈추고 스택을 되감으면서 지연된 호출을 실행합니다. 패닉의 원인, 지연 함수 안의 recover로 패닉을 멈추는 법, 자주 보는 런타임 오류 메시지, 그리고 패닉이 올바른 선택인 경우를 알아봅니다.

이 페이지에는 실행 가능한 에디터가 있습니다 - 편집하고 실행하면 결과를 바로 볼 수 있습니다.

패닉의 모습

패닉은 현재 함수를 멈추고 지연된 호출을 실행한 뒤, 호출자에서도 같은 일을 하며 스택을 따라 올라갑니다. 고루틴의 맨 위에 도달하면 프로그램이 죽습니다.

이 프로그램은 상태 코드 2로 종료됩니다. 출력은 다음과 같습니다:

before
deferred in main: still runs
panic: runtime error: index out of range [5] with length 3

goroutine 1 [running]:
main.main()
	/tmp/main.go:11 +0x...
exit status 2

지연된 호출은 충돌 보고 전에 실행되었습니다. 트레이스는 고루틴, 함수, 줄 번호를 알려 주며, 버그를 찾기에는 보통 이것으로 충분합니다.

흔한 런타임 패닉

메시지원인
index out of range [5] with length 3슬라이스, 배열, 문자열 인덱스가 끝을 넘음
slice bounds out of range [:7] with capacity 5용량을 넘어서 자름
invalid memory address or nil pointer dereferencenil 포인터로 필드를 읽거나 호출함
assignment to entry in nil map만든 적 없는 맵에 씀
interface conversion: interface {} is int, not string값 하나를 받는 타입 단언이 틀린 타입으로 향함
integer divide by zero정수를 0으로 나누거나 나머지 연산(실수는 대신 +InfNaN)
close of closed channel, send on closed channel채널 오용
all goroutines are asleep - deadlock!모든 고루틴이 블록됨(패닉이 아니라 치명적 오류)

모두 처리할 조건이 아니라 프로그램의 버그입니다. 해결책은 recover가 아니라 범위 검사, nil 검사, make, comma-ok 단언입니다.

복구하기

recover()는 패닉을 멈춥니다. 패닉이 되감기는 동안 실행되는 코드는 지연된 함수뿐이므로, 지연된 함수 안에서 직접 호출할 때만 동작합니다.

출력:

5 <nil>
0 recovered: runtime error: integer divide by zero
program continues

두 번째 호출에서 일어난 일:

  1. a / b가 패닉을 일으켰습니다.
  2. 지연된 클로저가 실행되었고, recover()가 패닉 값(runtime.Error)을 반환했습니다.
  3. 되감기가 멈췄습니다. safeDivide는 클로저가 설정한 이름 있는 결과 err와 함께 main으로 정상 반환되었습니다.

지연된 함수가 오류를 돌려줄 수 있는 것은 이름 있는 결과 덕분입니다. 이름이 없으면 함수는 제로 값을 반환합니다. 지연된 클로저가 결과를 수정하는 방법은 defer 페이지에서 다룹니다.

패닉이 없으면 recover()nil을 반환하므로, if r != nil 검사가 있으면 정상 경로에서 지연된 함수는 아무 해도 끼치지 않습니다. 지연된 함수 밖에서, 또는 지연된 함수가 호출한 함수 안에서 호출하면 recovernil을 반환하고 아무 일도 하지 않습니다.

직접 만든 값으로 panic하기

panic은 어떤 값이든 받습니다. 보통은 오류나 문자열을 넘깁니다.

예상한 것만 복구하고 나머지는 다시 panic하세요. 모든 패닉을 삼키면 진짜 버그가 숨겨집니다.

Go 1.21부터 panic(nil)*runtime.PanicNilError로 바뀌므로, 이제 recover()nil을 반환하면 확실히 "패닉 없음"을 뜻합니다.

고루틴의 패닉

recover는 자기 고루틴의 패닉만 잡습니다. recover가 없는 고루틴에서 패닉이 나면 main과 다른 모든 고루틴을 포함해 프로세스 전체가 죽습니다.

두 워커 줄은 어느 순서로든 출력될 수 있고, main finished는 항상 마지막에 나옵니다. maindefer recover()로는 두 번째 워커로부터 프로그램을 지킬 수 없었을 것입니다. HTTP 서버가 요청마다 복구하는 이유가 이것입니다. net/http는 각 핸들러 고루틴의 패닉을 복구해서 로그를 남기고 해당 연결을 닫으므로, 잘못된 요청 하나가 서버를 쓰러뜨리지 않습니다.

어떤 실패는 패닉이 아니라 치명적 오류라서 아예 복구할 수 없습니다. concurrent map writes, 메모리 부족, 교착 상태 탐지기의 all goroutines are asleep이 그렇습니다.

패닉이 올바른 선택일 때

Go의 일반 규칙은 이렇습니다. 런타임에 잘못될 수 있는 것에는 오류를 반환하고, 프로그래머의 실수에만 패닉을 씁니다. 구체적으로 다음 경우에 패닉이 적절합니다:

  • 불변식이 깨졌을 때. 직접 만든 열거형에 대한 switch가 일어날 수 없는 case에 도달했습니다. 계속 진행하면 데이터가 망가집니다.
  • Must 헬퍼가 잘못된 상수 입력을 받았을 때. regexp.MustCompile, template.Must, uuid.MustParse는 오류를 반환하는 함수를 감싸서 실패하면 패닉을 일으킵니다. 컴파일 타임에 알려진 값, 보통 패키지 수준 변수에 쓰세요. 이때 실패는 소스 코드가 틀렸다는 뜻입니다:
var emailRE = regexp.MustCompile(`^[^@\s]+@[^@\s]+$`)
  • 시작 과정을 계속할 수 없을 때. main에서 필수 설정이 없는 경우입니다. 이때도 스택 트레이스보다 오류를 출력하고 os.Exit(1)을 호출하는 편이 깔끔할 때가 많습니다.

다음에는 패닉이 맞지 않는 도구입니다:

  • 예상된 실패: 잘못된 사용자 입력, 없는 파일, 타임아웃. error를 반환하세요. 오류 처리를 참고하세요.
  • 제어 흐름: 큰 호출 트리에서 panic과 recover를 예외처럼 쓰면 코드를 따라가기 어려워집니다. 표준 라이브러리도 내부의 몇 곳(encoding/json 인코더)에서 이렇게 하지만, 항상 반환하기 전에 복구하므로 어떤 패닉도 패키지 밖으로 나가지 않습니다.
  • 라이브러리 API: 잘못된 입력에 패닉을 일으키는 라이브러리는 모든 호출자에게 recover를 강요합니다. 오류를 반환하세요.

흔한 실수

  • 지연된 함수 밖에서 recover를 호출함. nil을 반환합니다.
  • 고루틴의 패닉을 main에서 복구하려 함. 고루틴마다 자기 recover가 필요합니다.
  • 모든 패닉을 삼킴. 스택(runtime/debugdebug.Stack())과 함께 로그를 남기고, 예상하지 못한 것은 다시 panic하세요.
  • nil 맵 쓰기나 범위를 벗어난 인덱스를 recover로 처리함. 대신 버그를 고치세요.

자주 묻는 질문

Go에서 패닉이란 무엇인가요?

현재 고루틴의 정상 흐름을 멈추는 런타임 실패입니다. Go는 스택에 있는 각 함수의 지연된 호출을 가장 안쪽부터 바깥쪽으로 실행하고, 아무도 recover하지 않으면 프로그램이 패닉 값과 스택 트레이스를 출력한 뒤 상태 코드 2로 종료됩니다. 패닉은 버그(범위를 벗어난 인덱스, nil 포인터 역참조, nil 맵에 쓰기)나 명시적인 panic(v) 호출에서 발생합니다.

Go에서 패닉을 복구하려면 어떻게 하나요?

지연된 함수 안에서 recover()를 호출합니다: defer func() { if r := recover(); r != nil { ... } }(). panic에 넘긴 값을 반환하고 되감기를 멈추므로, 그 함수를 defer한 함수는 호출자에게 정상적으로 반환됩니다. 다른 곳에서 호출하면 recover는 nil을 반환하고 아무 일도 하지 않습니다.

다른 고루틴의 패닉을 복구할 수 있나요?

없습니다. recover는 자신이 실행되는 고루틴의 패닉만 멈춥니다. 직접 시작한 고루틴에서 패닉이 났는데 그 고루틴 안에 recover가 없으면 프로그램 전체가 죽습니다. 패닉이 날 수 있는 고루틴마다 자기만의 지연된 recover가 필요합니다.

오류를 반환하지 않고 panic을 써야 하는 때는 언제인가요?

예상된 실패가 아니라 버그와 불가능한 상태에 씁니다. 잘못된 입력, 없는 파일, 네트워크 오류는 오류입니다. 불변식이 깨졌을 때, 항상 유효해야 하는 상수가 Must 헬퍼에 주어졌을 때(regexp.MustCompile), 또는 프로그램이 아예 시작할 수 없을 때는 패닉이 타당합니다. 라이브러리는 패닉이 공개 API 밖으로 새어 나가게 해서는 안 됩니다.

Coddy programming languages illustration

Coddy로 코딩 배우기

시작하기