빠른 비교
| 표면 | 역할 |
|---|---|
error 반환 | 예상 가능한 실패를 호출자에게 돌려준다 |
panic | 현재 실행을 중단하고 stack을 되감는다 |
defer | 함수가 끝날 때 실행할 정리 작업을 등록한다 |
recover | deferred 함수 안에서 panic 전파를 멈춘다 |
실패 경계
일반 실패는 error로 반환한다
Go에서 파일 없음, 입력 검증 실패, 네트워크 오류, DB 오류처럼 호출자가 처리할 수 있는 실패는 error로 반환하는 것이 기본입니다. panic은 일반 제어 흐름이 아니라 프로그램 상태가 더 이상 정상적으로 이어질 수 없거나, goroutine 경계에서 보호 장치를 둬야 하는 경우에 제한적으로 씁니다.
func load(path string) ([]byte, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
return data, nil
}라이브러리 함수가 입력 오류마다 panic을 내면 호출자는 복구 가능한 실패를 다루기 어려워집니다. public API에서는 panic보다 error 반환을 먼저 검토합니다.
defer는 정리 작업을 함수 종료 시점으로 미룬다
defer는 현재 함수가 return되거나 panic으로 빠져나갈 때 실행됩니다. 파일 close, lock unlock, cancel 호출처럼 반드시 정리해야 하는 작업에 적합합니다. 여러 defer가 등록되면 나중에 등록한 것부터 실행됩니다.
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()defer의 인수는 등록 시점에 평가됩니다. 반복문 안에서 많은 defer를 쌓으면 함수가 끝날 때까지 자원이 닫히지 않을 수 있으므로, 루프 내부 자원은 별도 함수로 분리하는 편이 안전합니다.
recover는 deferred 함수 안에서만 panic을 잡는다
recover()는 deferred 함수 안에서 호출될 때만 현재 goroutine의 panic을 멈출 수 있습니다. 일반 함수 호출 위치에서 recover()를 호출해도 nil을 반환합니다. 또한 recover는 다른 goroutine에서 발생한 panic을 잡지 못합니다.
func safeRun(fn func()) (err error) {
defer func() {
if v := recover(); v != nil {
err = fmt.Errorf("panic: %v", v)
}
}()
fn()
return nil
}recover는 panic을 숨기기 위한 도구가 아니라, goroutine 경계나 HTTP middleware 같은 경계에서 로그를 남기고 프로세스 전체 장애를 막는 안전망입니다.
선택 기준
| 상황 | 먼저 고를 방식 |
|---|---|
| 호출자가 처리할 수 있는 실패 | error 반환 |
| 반드시 실행해야 하는 정리 작업 | defer |
| 초기화 실패로 계속 실행 불가 | 제한적 panic |
| HTTP 요청 단위 보호 | recover middleware |
| goroutine 내부 panic 보호 | goroutine 내부에서 recover |
주의할 점
recover는 panic이 발생한 goroutine 안의 deferred 함수에서만 동작합니다. 부모 goroutine에서 자식 goroutine의 panic을 recover할 수 없습니다. goroutine을 직접 띄우는 경계라면 그 goroutine 내부에 recover와 로그 처리를 함께 둬야 합니다.
panic을 error 처리 대신 남용하면 테스트와 운영 진단이 어려워집니다. 복구 가능한 입력 오류, 외부 시스템 오류, 비즈니스 검증 실패는 대부분 error로 표현하는 편이 Go 코드의 호출 흐름과 맞습니다.
참고 링크
2 sources