빠른 비교
err := errors.Join(
closeFile(),
flushMetrics(),
shutdownServer(ctx),
)
if err != nil {
return err
}| 패턴 | 용도 |
|---|---|
fmt.Errorf("...: %w", err) | 단일 원인에 문맥 추가 |
errors.Join(a, b, c) | 독립 실패 여러 개를 함께 반환 |
errors.Is | error tree 안의 sentinel 검사 |
errors.As | error tree 안의 구체 타입 추출 |
에러 묶음
errors.Join은 여러 error를 하나의 error로 만든다
여러 cleanup, 여러 goroutine 결과, 여러 검증 오류처럼 독립된 실패가 동시에 나올 수 있습니다. 이때 첫 번째 error만 반환하면 나머지 실패가 사라집니다. errors.Join은 nil이 아닌 error들을 묶어 하나의 error로 반환합니다.
func shutdown(ctx context.Context) error {
return errors.Join(
stopHTTP(ctx),
closeDB(),
flushLogs(),
)
}모든 인자가 nil이면 결과도 nil입니다. 호출자는 여전히 일반 error 하나만 받지만, 내부적으로는 여러 자식 error를 가진 tree처럼 검사됩니다.
errors.Is와 errors.As는 joined error도 탐색한다
Go의 error 검사는 단순 문자열 비교가 아니라 error tree 탐색입니다. errors.Is는 Unwrap() error뿐 아니라 Unwrap() []error로 묶인 여러 error도 따라가며 target과 맞는지 확인합니다.
err := errors.Join(
fmt.Errorf("cache: %w", ErrUnavailable),
fmt.Errorf("db: %w", context.DeadlineExceeded),
)
if errors.Is(err, context.DeadlineExceeded) {
return retryLater(err)
}구체 error 타입이 필요하면 errors.As를 씁니다. joined error 안에 해당 타입이 있으면 target에 대입됩니다.
var pathErr *os.PathError
if errors.As(err, &pathErr) {
log.Println(pathErr.Op, pathErr.Path)
}Join은 순서 있는 실패 보고가 아니라 실패 집합 표현이다
errors.Join의 문자열 출력은 사람이 읽는 로그에는 유용하지만, 프로그램 분기 기준으로 쓰면 취약합니다. 검사 기준은 errors.Is, errors.As, 별도 typed error가 되어야 합니다.
if strings.Contains(err.Error(), "deadline") {
// 취약한 분기
}
if errors.Is(err, context.DeadlineExceeded) {
// 안정적인 분기
}작업 순서가 중요하고 "첫 실패에서 중단"해야 하는 흐름이면 Join보다 즉시 return이 더 명확합니다. Join은 가능한 작업을 모두 시도한 뒤 실패를 모아서 보고해야 할 때 적합합니다.
선택 기준
| 상황 | 적합한 선택 |
|---|---|
| 하나의 원인에 문맥 추가 | %w wrapping |
| 여러 cleanup 실패를 모두 보고 | errors.Join |
| validation 오류 여러 개 반환 | typed error list 또는 errors.Join |
| 첫 실패에서 바로 중단해야 함 | 즉시 return err |
| 호출자가 특정 원인을 검사해야 함 | errors.Is / errors.As 유지 |
multi-error를 공개 API로 반환할 때는 호출자가 무엇을 할 수 있어야 하는지 먼저 정해야 합니다. 단순 로그용이면 Join으로 충분하지만, 필드별 검증 오류처럼 구조가 중요하면 별도 타입을 제공하는 편이 낫습니다.
주의할 점
joined error의 Error() 문자열은 분기 계약이 아닙니다.
여러 줄 문자열이나 포맷은 바뀔 수 있으므로, 코드 흐름은 errors.Is와 errors.As로 결정해야 합니다.
err := errors.Join(ErrConfig, ErrNetwork)
switch {
case errors.Is(err, ErrConfig):
return "config"
case errors.Is(err, ErrNetwork):
return "network"
}errors.Join은 중복 error를 자동으로 의미 있게 병합하지 않습니다. 같은 원인이 여러 번 발생하면 여러 자식으로 들어갈 수 있으므로, 사용자에게 보여 줄 메시지는 필요하면 별도 정규화 단계를 두는 편이 좋습니다.
참고 링크
1 sources