Go는 왜 try/catch가 없을까 — error 코드 반환과 panic/recover 선택 기준
1. 문제 정의
Java, C#, Python, C++에 익숙한 개발자가 처음 Go 코드를 접하면 흔히 이런 질문을 던집니다.
“Go 언어에서 예외는 어떻게 처리하나요? 왜 try/catch가 없나요?”
StackOverflow의 Exception handling in Google Go language (7198037)는 정확히 이 고민에서 출발합니다. 질문자는 전통적인 try/catch 예외 처리에 익숙한 상태에서 Go의 에러 처리 스타일이 갖는 장단점을 묻습니다.
핵심은 단순히 “Go에 예외 문법이 없다"가 아니라, Go는 명시적 error 코드 반환을 관용구로 채택했고, panic/recover는 기능적으로 try/catch와 동등하되 ‘의도된 사용 범위’가 다르다는 점입니다. 이 차이는 기능이 아니라 의도된 용법이라는 것이 이 글의 중심 주제입니다.
검증 환경:
go1.18.1,linux/arm64(Ubuntu 22.04). 본문의 실행 출력은 모두 이 환경에서go run으로 직접 확인한 실제 결과입니다.
2. 빠른 진단 체크리스트
“지금 마주한 상황에서 어떤 에러 처리 방식을 써야 하는가"를 판단할 때 아래 순서대로 체크하세요. 앞선 항목에서 결정이 나면 뒤쪽은 건너뛰어도 됩니다.
| 순서 | 질문 | 해당하면 선택 |
|---|---|---|
| 1 | 이 실패가 예측 가능한 정상적 오류인가? (파일 없음, 네트워크 실패, 잘못된 입력) | error 반환 (명시적 처리) |
| 2 | 이 실패가 복구 불가능·프로그래머 실수(버그) 로 인한 상태 불변식 위반인가? | panic |
| 3 | 패키지 내부에서 unwind만 필요하고 외부 API는 error로 노출해야 하는가? | 내부 panic + 경계에서 recover |
| 4 | 단순히 “try/catch처럼 예외를 잡아 계속 진행"이 목적인가? | 다시 보기 — 순서 1·2를 먼저 판단 |
| 5 | 그럼에도 예외 흐름을 잡아 그 함수의 defer 정리는 실행하고 싶은가? | panic + defer + recover |
주의: 순서 4가 가장 흔한 함정입니다. try/catch 문화에서는 “예외를 잡아 잠깐 처리하고 흐름을 계속"하는 게 자연스럽지만, Go에서 기대되는 방식은 복구 가능 여부를 먼저 가르는 것입니다. 이 체크리스트는 일반적인 점검 기준이며 프로젝트 코딩 컨벤션에 따라 달라질 수 있습니다.
3. 원인 후보별 확인 방법 — 함께 오는 오해
가장 흔한 오해 세 가지를 실제 Go 동작으로 확인합니다.
원인 후보 1 — “panic/recover는 try/catch의 등가물이니 똑같이 쓴다” (부분 사실 + 용법 차이)
panic은 현재 함수의 실행을 멈추고 그 함수의 모든 defer를 실행한 뒤 호출자로 전파되며, goroutine 콜 스택을 따라 올라가 프로그램을 종료시킵니다. recover는 defer 함수 안에서만 의미를 가지며, panic 중인 goroutine의 제어권을 되찾습니다. 공식 Go Blog에 따르면 panic/recover는 try/catch의 “도덕적 등가물(moral equivalent)“입니다.
기능적으로는 동일하지만 의도된 사용 방식이 다릅니다. Go 표준 라이브러리를 보면 대부분의 panic은 치명적 오류, 즉 라이브러리 내부 버그이거나 잘못된 데이터를 넘겨받은 경우(예: JSON 디코딩 함수에 잘못된 JSON 전달)를 알리는 데 쓰입니다. 이 관용구에서는 recover로 “잡아서 계속 진행"하는 것이 아니라, 재귀 구조를 unwind하고 경계에서 error로 바꾸는 용도로만 제한됩니다.
원인 후보 2 — “예외가 없는 언어라서 에러 처리가 안 된다” (오해)
Go의 error는 단순 interface입니다. 함수는 다중 반환값을 지원하므로 (값, err) 패턴을 문법적으로 쉽게 만들 수 있습니다.
| |
호출 측은 err를 명시적으로 검사합니다. 검증 환경 go1.18.1 linux/arm64에서 실제 실행 결과 (go run):
| |
이처럼 오류가 “던져지는 것"이 아니라 “값으로 돌아와 명시적으로 검사되는 것"이 Go의 관용구입니다. 다중 반환 덕에 문법 비용이 낮아져, 예외가 대체재로 필요하지 않습니다.
원인 후보 3 — “panic을 잡으면 try/catch처럼 쓸 수 있으니 마음껏 쓰자” (권장되지 않음)
panic을 recover로 잡는 것은 가능하지만, 이를 일상적인 오류 흐름 제어에 남용하면 복구 불가능해야 할 버그를 삼키고 잘못된 값으로 실행을 계속하게 됩니다. 아래 4-3에서 실제 실패 예제로 확인합니다.
4. 해결 방법 — 상황별 코드
4-1. 예측 가능한 오류: error 반환 (기본)
네트워크 실패, 파일 열기 실패, 입력 검증 실패는 모두 호출자가 복구 여부를 결정할 수 있어야 하므로 명시적 error 반환이 관용구입니다.
| |
4-2. 복구 불가능한 상태: panic
프로그래머의 실수(버그)로 상태 불변식이 깨진 경우, 호출자가 의미 있게 처리할 수 없으므로 panic이 적절합니다.
| |
4-3. 실패한 해결책 — panic을 try/catch처럼 recover로 남용한 코드
가장 흔한 잘못된 해결책은 “예외를 잡아 계속 진행하면 안전하다"는 습관을 그대로 옮겨, 복구 불가능한 버그의 panic을 전부 삼키는 recover 래퍼를 두는 것입니다. 아래 코드는 그 전형입니다.
| |
검증 환경 go1.18.1 linux/arm64에서 실제 실행 결과 (go run wrongfix.go):
| |
critical(false)가 불변식 위반으로 panic을 일으켰음에도, swallowAll이 그 panic을 남겨진 v(초기값 0)로 계속 계산을 진행합니다. 복구 불가능한 버그가 데이터 오염(계산 결과 = 0)으로 변환되어 겉으로는 정상 종료(exit 0)처럼 보입니다. 이 때문에 panic을 잡아 “계속 진행"하는 것은 권장하지 않습니다. panic이 의미 있는 경우는 4-2처럼 버그를 그대로 드러내 종료시키거나, 4-4처럼 unwind 뒤 경계에서 error로 환원할 때뿐입니다.
교정 방향: 복구 가능 여부를 먼저 판단합니다. critical이 실제로 버그라면 panic을 삼키지 말고 그대로 드러내야 합니다. 값이 조용히 0으로 감는 대신 스택 추적과 함께 종료되어야 프로덕션 전에 잡힙니다.
4-4. 내부 unwind + 외부 error 노출: panic/recover 조합 (허용되는 용법)
Go Blog가 인용하는 대표 사례는 표준 라이브러리 encoding/json입니다. 재귀 호출로 값을 순회하다 오류가 나면 panic으로 콜 스택을 top-level까지 풀고, 최상위 함수에서 recover하여 적절한 error 값을 반환합니다.
표준 라이브러리 규약: “패키지가 내부적으로 panic을 쓰더라도, 외부 API는 여전히 명시적 error 반환값을 제공한다.”
즉, 재귀 구조에서 unwind가 필요하면 내부에서 panic을 쓰고 경계(boundary)에서 recover하여 error로 바꾸는 패턴이 허용되지만, 이는 구현 상세이고 외부 사용자에게는 error가 노출되어야 합니다. 아래는 recover가 panic을 잡아도 전체 함수가 살아남는 실제 동작 확인입니다.
| |
검증 환경 go1.18.1 linux/arm64에서 실제 실행 결과 (go run):
| |
defer 안의 recover가 panic 제어권을 되찾아 함수가 계속 동작함을 확인할 수 있습니다. 다만 이 능력은 위 실패 예제(4-3)처럼 “무차별 삼키기"가 아니라 경계에서 error로 환원하는 용법에만 쓰는 것이 규칙입니다.
4-5. os.Exit — 즉시 종료 (이 글의 범위 밖, 전용 글 참고)
os.Exit은 어디서 호출되든 defer를 실행하지 않고 즉시 프로세스를 종료합니다. 이 글의 주제(try/catch 부재와 error vs panic/recover 의도)와 겹치지 않도록, os.Exit vs panic 선택 기준은 이 글에서 자세히 다루지 않고 “즉시 종료 + 종료 코드 제어"가 목적이라면 os.Exit을 고려한다는 것만 짚고 넘어갑니다.
5. 검증 명령 (실제 실행 결과 포함)
해결책을 적용한 뒤 이 글의 검증 환경(go1.18.1 linux/arm64, Ubuntu 22.04)에서 아래 명령을 사용해 확인합니다.
| |
실제 실행 결과:
| |
위 출력은 이 글을 작성하는 과정에서 실제로 명령을 수행해 얻은 결과입니다.
panic/recover및 잘못된recover남용을 검증할 때는 4-3·4-4의 예제를 잇따라go run하여 “계속 진행 vs 정상 복구"가 갈리는지 확인하시기 바랍니다.
6. 버전별 차이
Go의 에러 처리 도구(error 반환, panic/recover, defer)는 초기 문서가 작성된 이후 지금까지 동작이 안정적으로 유지되는 핵심 언어 기능입니다. 아래 표는 이 글의 검증 환경(go1.18.1, linux/arm64)과 공식 Go 문서 기준으로 정리한 것입니다.
| 항목 | 초기 Go 문서(2010~2011) | Go 1.18.1 검증 | 호환성 |
|---|---|---|---|
error 타입 (interface 선언) | error interface { Error() string } | 동일하게 동작 (재현 실행 확인) | ✅ 안정 |
(값, err) 다중 반환 패턴 | 관용구로 소개 | 동일 | ✅ 안정 |
panic/recover/defer | “try/catch의 도덕적 등가물"로 소개 | 동일 의미로 동작 (4-4 실행 확인) | ✅ 안정 |
any vs interface{} 표기 | interface{}/any 혼용 설명 | any는 Go 1.18에서 도입된 별칭 | ⚠️ 표기만 다름 (의미 동일) |
주의할 점:
any는 Go 1.18에서 도입된interface{}의 별칭입니다. 오래된 글은interface{}를, 최신 코드는any를 씁니다. 동작은 같으며, 이 글의 검증 환경(go1.18.1)에서는 둘 다 사용할 수 있습니다.error타입을 별도로 재선언할 필요는 없습니다. 내장error타입과fmt.Errorf를 그대로 쓰는 것이 관용구이며, 커스텀으로error interface를 다시 선언하면 오히려 혼란을 줍니다.defer의 정리 용법(파일·락·버퍼)은 모든 버전에서 유효하지만,os.Exit이 defer를 건너뛴다는 동작은 버전과 무관하게 항상 유지되므로 즉시 종료 전 정리가 필요한지 먼저 판단해야 합니다.
위 내용은 일반적인 점검 기준이며, 프로젝트가 사용하는 Go 버전에 따라 표기 스타일이 달라질 수 있습니다.
7. 재발 방지 체크
Go 커뮤니티 컨벤션과 공식 문서 기준의 예방 지침입니다.
- 생각의 전환: 예외는 “처리되지 않으면 터진다"가 아니라 error는 값이므로 흐름이 명시적이라는 사고로 전환합니다.
- panic은 정말 드물게만: 라이브러리 내부 버그나 사전 조건 위반 수준에서만 고려합니다. 외부 API로는 항상 error를 노출합니다.
- recover의 남용 금지: “잡아서 계속 진행"을 위해 쓴
recover는 버그를 데이터 오염으로 바꿉니다(4-3). unwind → 경계에서 error로 환원할 때만 씁니다. - 자원 정리는 try/finally 대신 defer: 파일·락·버퍼 정리는 열자마자
defer로 등록하는 것이 안전합니다. - 에러에 문맥 추가:
fmt.Errorf("...: %w", err)로 감싸 호출 위치 문맥을 남깁니다.
| 오류 성격 | 권장 방식 | 핵심 이유 |
|---|---|---|
| 예측 가능한 오류 (없는 파일, 네트워크, 입력) | error 반환 | 호출자가 복구 결정 |
| 복구 불가·프로그래머 실수 (불변식 위반) | panic | 버그를 그대로 드러내 종료 |
| 재귀 unwind + 외부 error | 내부 panic + 경계 recover | 경계에서 error로 환원 |
| try/catch처럼 “잡아 계속 진행” | 권장하지 않음 | 버그를 삼켜 데이터 오염 (4-3) |
| 즉시 종료 + 종료 코드 | os.Exit | defer 미실행 주의 (전용 글 참고) |
DevTrace 독자 결론
Go에서 error 코드 반환과 panic/recover의 차이는 **기능이 아니라 ‘의도된 사용 범위’**이며, try/catch가 없어서 아쉽다는 관점보다는 예측 가능한 오류는 명시적 error로, 복구 불가능한 상태는 panic으로 드러내고, unwind가 필요할 때만 경계에서 recover로 error로 환원하는 것이 Go 관용구의 정수입니다. panic을 try/catch처럼 남용해 recover로 삼키면 버그가 데이터 오염으로 변환되는 것(4-3에서 확인)이 이 선택의 실제 비용입니다.
출처:
- StackOverflow — Exception handling in Google Go language (7198037)
- Go Blog — Defer, Panic, and Recover
- Go Blog — Error handling and Go
본문에서 “일반적인 점검 기준”·“일반적인 설계 패턴"으로 표시된 내용과 선택 기준 표는 원문 출처에 없는 독자 분석이며, 검증 환경(go1.18.1 linux/arm64)·프로젝트 코딩 컨벤션에 따라 달라질 수 있습니다. 모든 실행 출력은 명시된 환경에서 직접 수행해 확인한 결과입니다.