핵심 정리
user, err := repo.Find(id)
if err != nil {
return fmt.Errorf("load user %s: %w", id, err)
}Go의 에러 처리는 "예외를 던진다"보다 반환값으로 실패를 드러내고, 문맥은 wrapping으로 붙인다로 읽는 편이 맞습니다.
기본 흐름
기본 계약은 (T, error)다
func FindUser(id string) (User, error) {
// ...
}호출자는 거의 항상 아래 흐름으로 읽습니다.
user, err := FindUser("u-1")
if err != nil {
return err
}즉 정상값과 실패 경로를 함께 돌려주는 것이 Go의 기본 계약입니다.
wrapping은 원인을 버리지 않고 문맥을 붙인다
fmt.Errorf("...: %w", err)를 쓰면 에러 원인을 유지한 채 바깥 문맥을 덧붙일 수 있습니다.
if err != nil {
return fmt.Errorf("open config: %w", err)
}이렇게 해야 위쪽 호출자가 "어디서 실패했는지"와 "실제 원인이 무엇인지"를 둘 다 볼 수 있습니다.
비교는 문자열보다 errors.Is, errors.As를 먼저 본다
if errors.Is(err, os.ErrNotExist) {
// 파일 없음 처리
}구체 타입이 필요할 때는 errors.As를 씁니다.
var pathErr *os.PathError
if errors.As(err, &pathErr) {
// pathErr.Path, pathErr.Op 사용
}에러 메시지 문자열 비교는 로그에는 쓸 수 있어도 제어 흐름 기준으로는 약합니다.
언제 panic을 쓸까
대부분의 일반 실패는 error 반환으로 충분합니다. panic은 보통 복구 불가능한 프로그래머 실수나 초기화 불변식 붕괴처럼 범위를 더 좁게 봅니다.
- 파일 없음, 입력 오류, 외부 API 실패:
error - 정말 계속 진행하면 안 되는 내부 붕괴: 제한적으로
panic
선택 기준
| 상황 | 먼저 떠올릴 선택 |
|---|---|
| 일반 실패 보고 | error 반환 |
| 상위 호출자에 문맥 추가 | %w wrapping |
| sentinel error 비교 | errors.Is |
| 구체 에러 타입 추출 | errors.As |
| 복구 불가능한 내부 붕괴 | 제한적 panic |
주의할 점
Go에서 흔한 실수는 에러를 문자열로만 덮어써 원인을 잃는 것입니다. 문맥을 붙여야 할 때는 %w로 감싸고, 비교는 errors.Is/errors.As로 읽는 흐름을 기본값으로 두는 편이 안전합니다.
참고 링크
3 sources