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, &notFound) {
    // 오류를 처리
  } 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("테스트 설정 실패")
}