Golang 오류 처리 사양
오류 유형
오류를 선언하는 몇 가지 옵션이 있습니다. 사용 사례에 가장 적합한 옵션을 선택하기 전에 다음 사항을 고려하십시오.
- 호출자가 오류를 처리하려면 일치해야합니까? 그렇다면
errors.Is
또는errors.As
함수를 지원해야하며, 이를 위해 최상위 오류 변수 또는 사용자 정의 유형을 선언해야 합니다. - 오류 메시지가 정적 문자열인지 아니면 맥락 정보가 필요한 동적 문자열인가요? 정적 문자열의 경우
errors.New
를 사용할 수 있지만, 후자의 경우fmt.Errorf
또는 사용자 정의 오류 유형을 사용해야 합니다. - 하위 함수에서 반환된 새로운 오류를 전달하고 있나요? 그렇다면 오류 래핑 섹션을 참조하십시오.
오류 일치 여부? | 오류 메시지 | 지침 |
---|---|---|
없음 | 정적 | errors.New |
없음 | 동적 | fmt.Errorf |
예 | 정적 | errors.New 로 최상위 var 선언 |
예 | 동적 | 사용자 정의 error 유형 |
예를 들어, 정적 문자열을 가진 오류를 나타내려면 errors.New
를 사용하십시오. 호출자가 이 오류와 일치시키고 처리해야하는 경우에는 errors.Is
와 일치하기 위해 변수를 내보내어 지원해야합니다.
오류 일치 없음
// package foo
func Open() error {
return errors.New("could not open")
}
// package bar
if err := foo.Open(); err != nil {
// 오류를 처리할 수 없음.
panic("알 수 없는 오류")
}
오류 일치
// package foo
var ErrCouldNotOpen = errors.New("could not open")
func Open() error {
return ErrCouldNotOpen
}
// package bar
if err := foo.Open(); err != nil {
if errors.Is(err, foo.ErrCouldNotOpen) {
// 오류 처리
} else {
panic("알 수 없는 오류")
}
}
동적 문자열을 가진 오류의 경우 호출자가 일치시키지 않는다면 fmt.Errorf
를 사용하십시오. 호출자가 일치시켜야 한다면 사용자 정의 error
를 사용하십시오.
오류 일치 없음
// package foo
func Open(file string) error {
return fmt.Errorf("file %q not found", file)
}
// package bar
if err := foo.Open("testfile.txt"); err != nil {
// 오류를 처리할 수 없음.
panic("알 수 없는 오류")
}
오류 일치
// package foo
type NotFoundError struct {
File string
}
func (e *NotFoundError) Error() string {
return fmt.Sprintf("file %q not found", e.File)
}
func Open(file string) error {
return &NotFoundError{File: file}
}
// package bar
if err := foo.Open("testfile.txt"); err != nil {
var notFound *NotFoundError
if errors.As(err, ¬Found) {
// 오류를 처리
} else {
panic("알 수 없는 오류")
}
}
패키지에서 오류 변수나 유형을 내보내면 해당 패키지의 공개 API의 일부가 됨을 유의하십시오.
오류 래핑
다른 메소드를 호출하는 동안 오류가 발생하면 일반적으로 세 가지 방법으로 처리할 수 있습니다.
- 원래 오류를 그대로 반환합니다.
-
fmt.Errorf
를 사용하여%w
를 사용하여 오류에 컨텍스트를 추가한 다음 반환합니다. -
fmt.Errorf
를 사용하여%v
를 사용하여 오류에 컨텍스트를 추가한 다음 반환합니다.
추가적인 컨텍스트를 추가할 내용이 없으면, 원래 오류를 그대로 반환하십시오. 이렇게 하면 원래 오류 유형과 메시지가 보존됩니다. 이것은 근본적으로 오류가 어디에서 발생했는지를 추적하는 데 충분한 정보가 포함된 경우에 특히 적합합니다.
그렇지 않으면 "연결 거부됨"과 같은 모호한 오류가 발생하지 않도록 가능한 한 오류 메시지에 컨텍스트를 추가하십시오. 대신 "서비스 foo 호출 중: 연결 거부됨"과 같이 더 유용한 오류를 받게 됩니다.
오류에 컨텍스트를 추가하려면 오류에 대한 컨텍스트를 최대한 추가하여 호출자가 루트 원인을 일치시킬 수 있는지 여부에 따라 %w
또는 %v
동사를 선택하고 이에 따라 선택하십시오.
- 호출자가 기본 오류에 액세스해야 하는 경우
%w
를 사용하십시오. 이는 대부분의 래핑 오류에 대한 좋은 기본값입니다. 그러나 호출자가 이러한 동작에 의존하기 시작할 수 있음을 인지하십시오. 따라서 알려진 변수 또는 유형에 대한 래핑 오류의 경우 함수 계약의 일부로 레코드를 남겨 테스트하십시오. - 호출자가 기본 오류를 은폐해야 하는 경우
%v
를 사용하십시오. 호출자는 이를 일치시킬 수 없지만 필요에 따라%w
로 전환할 수 있습니다.
반환된 오류에 컨텍스트를 추가할 때, 컨텍스트를 간결하게 유지하기 위해 "failed to"와 같은 구문을 사용하지 마십시오. 오류가 스택을 통해 퍼져 나갈 때, 스택에는 레이어별로 쌓일 것입니다.
추천하지 않는 방법:
s, err := store.New()
if err != nil {
return fmt.Errorf(
"failed to create new store: %w", err)
}
// failed to x: failed to y: failed to create new store: the error
권장하는 방법:
s, err := store.New()
if err != nil {
return fmt.Errorf(
"new store: %w", err)
}
// x: y: new store: the error
그러나, 오류가 다른 시스템으로 전송되면 메시지가 오류임을 명확히해야 합니다(예: 로그에서 "err" 태그 또는 "실패" 접두사).
잘못된 명명
전역 변수로 저장된 오류 값의 경우, 내보내는지 여부에 따라 접두사 Err
또는 err
을 사용하십시오. 가이드라인을 참고하십시오. 내보내지 않은 최상위 상수 및 변수의 경우 접두사로 밑줄 (_)을 사용하십시오.
var (
// 다음 두 오류를 내보내어 이 패키지의 사용자가 errors.Is로 일치시킬 수 있도록 합니다.
ErrBrokenLink = errors.New("link is broken")
ErrCouldNotOpen = errors.New("could not open")
// 이 오류는 공개 API의 일부가 되지 않도록 내보내지 않았습니다. 그러나 해당 패키지 내에서 errors를 사용할 수 있습니다.
errNotFound = errors.New("not found")
)
사용자 정의 오류 유형의 경우 접미사로 Error
를 사용하십시오.
// 또한 이 오류는 내보내어 이 패키지의 사용자가 errors.As로 일치시킬 수 있도록 합니다.
type NotFoundError struct {
File string
}
func (e *NotFoundError) Error() string {
return fmt.Sprintf("file %q not found", e.File)
}
// 이 오류는 공개 API의 일부가 되지 않도록 내보내지 않았습니다. 그러나 해당 패키지 내에서 errors.As를 사용할 수 있습니다.
type resolveError struct {
Path string
}
func (e *resolveError) Error() string {
return fmt.Sprintf("resolve %q", e.Path)
}
오류 처리
호출자가 호출된 함수에서 오류를 받으면, 오류의 이해에 따라 다양한 방법으로 오류를 처리할 수 있습니다.
이에는 다음과 같은 것이 포함됩니다.
- 호출된 함수가 특정 오류 정의에 동의했다면
errors.Is
또는errors.As
와 일치시켜 분기 처리하고 - 오류가 복구 가능하다면 오류를 로깅하고 gracefully degrading
- 도메인 특정 실패 조건을 나타내는 경우 잘 정의된 오류를 반환하거나
- 오류가 감싸져 있거나 그대로인 경우에 오류를 반환
호출자가 오류를 처리하는 방식에 관계없이 보통 각 오류에 대해 한 번만 처리해야 합니다. 예를 들어, 호출자는 오류를 기록한 다음에 반환해서는 안 되며, 왜냐하면 그 호출자가 또한 그 오류를 처리할 수 있기 때문입니다.
예를 들어, 다음 시나리오를 고려해보겠습니다:
나쁜 예: 오류를 기록하고 반환하기
스택 상위의 다른 호출자들은 이 오류에 대해 유사한 조치를 취할 수 있습니다. 이는 애플리케이션 로그에 많은 소음을 만들고 별다른 혜택이 없습니다.
u, err := getUser(id)
if err != nil {
// 나쁨: 설명 참조
log.Printf("%q 사용자를 가져오지 못했습니다: %v", id, err)
return err
}
좋은 예: 오류를 감싸고 반환하기
스택 상위의 다른 호출자들은 이 오류를 처리할 것입니다. %w
를 사용하여 errors.Is
또는 errors.As
와 일치시킬 수 있도록 확실하게 합니다.
u, err := getUser(id)
if err != nil {
return fmt.Errorf("%q 사용자 가져오기: %w", id, err)
}
좋은 예: 오류를 기록하고 gracefully degrading하기
작업이 절대적으로 필요하지 않다면, 경험을 중단시키지 않고 복구하여 grcefully degradation을 제공할 수 있습니다.
if err := emitMetrics(); err != nil {
// 메트릭 기록 실패는 응용 프로그램을 중단시켜서는 안 됩니다.
log.Printf("메트릭을 기록할 수 없음: %v", err)
}
좋은 예: 오류 일치시키고 적절하게 gracefully degrading하기
호출된 함수가 계약에 특정 오류를 정의했을 경우, 그 오류 사례와 gracefully degrade해야 합니다. 그 외의 경우에는 오류를 감싸고 반환해야 합니다. 스택 상위의 오류를 처리할 것입니다.
tz, err := getUserTimeZone(id)
if err != nil {
if errors.Is(err, ErrUserNotFound) {
// 사용자가 존재하지 않습니다. UTC를 사용합니다.
tz = time.UTC
} else {
return fmt.Errorf("%q 사용자 가져오기: %w", id, err)
}
}
단언 실패 처리
타입 단언은 타입 감지가 잘못된 경우에는 하나의 반환 값으로 패닉을 일으킵니다. 따라서 항상 "쉼표, OK" 관용구를 사용해야 합니다.
권장하지 않음:
t := i.(string)
권장:
t, ok := i.(string)
if !ok {
// 오류를 gracefully 처리
}
패닉 사용을 피하십시오
프로덕션 환경에서 실행되는 코드는 패닉을 피해야합니다. 패닉은 카스케이딩 실패의 주요 원인입니다. 오류가 발생하면 함수는 오류를 반환하고 호출자가 어떻게 처리할지 결정하도록 해야 합니다.
비권장:
func run(args []string) {
if len(args) == 0 {
panic("인수가 필요합니다")
}
// ...
}
func main() {
run(os.Args[1:])
}
권장:
func run(args []string) error {
if len(args) == 0 {
return errors.New("인수가 필요합니다")
}
// ...
return nil
}
func main() {
if err := run(os.Args[1:]); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
패닉/복구는 오류 처리 전략이 아닙니다. 패닉은 (예: nil 참조) 복구할 수 없는 이벤트가 발생할 때에만 발생해야합니다. 프로그램 초기화 중에 예외는 프로그램 시작 중에 처리되어야 합니다.
var _statusTemplate = template.Must(template.New("name").Parse("_statusHTML"))
테스트 코드에서도 실패가 표시되도록 t.Fatal
또는 t.FailNow
를 사용하는 것이 바람직합니다.
비권장:
// func TestFoo(t *testing.T)
f, err := os.CreateTemp("", "test")
if err != nil {
panic("테스트 설정 실패")
}
권장:
// func TestFoo(t *testing.T)
f, err := os.CreateTemp("", "test")
if err != nil {
t.Fatal("테스트 설정 실패")
}