오류

오류 에서 Go

2개의 연습 문제

오류 소개

오류 인터페이스

Go에서는 오류를 예외로 처리하지 않아요. 대신, 오류는 내장 error 인터페이스를 구현하는 타입의 평범한 _값_이에요. error 인터페이스는 아주 최소한이에요. 오류 메시지를 문자열로 반환하는 Error() 메서드 하나만 가지고 있어요.

type error interface {
  Error() string
}

실행 중에 발생한 오류가 호출한 쪽까지 전달되어야 하는 함수를 정의할 때는, 반환 타입 중 하나로 error를 포함해야 해요. 함수가 여러 값을 반환한다면, 관례상 error는 항상 마지막에 와요.

func DoSomething() (int, error) {
  // ...
}

간단한 오류 만들고 반환하기

오류 인터페이스를 항상 직접 구현해야 하는 것은 아니에요. 간단한 오류를 만들려면 표준 라이브러리 패키지 errors에 포함된 errors.New() 함수를 사용하면 돼요. 오류 메시지를 문자열로 전달하기만 하면, errors.New()가 메시지를 담고 error 인터페이스를 구현하는 값을 알아서 만들어 줘요.

함수가 오류를 반환한다면, 다른 모든 반환 값에 대해서는 제로 값을 반환하는 것이 좋은 관행이에요:

func DoSomething() (SomeStruct, int, error) {
  // ...
  return SomeStruct{}, 0, errors.New("failed to calculate result")
}
Caution

오류가 있을 때 모든 함수가 다른 반환 값으로 제로 값을 반환한다고 가정하면 안 돼요. 오류가 반환되면 다른 반환 값은 어느 것도 안전하게 사용할 수 없다고 가정하는 것이 가장 좋아요.

예를 들어, 어떤 함수가 파일을 읽으려다가 절반만 채워진 바이트 슬라이스와 오류를 반환했다고 생각해 봐요. 오류가 있었으니 반환된 바이트 슬라이스가 항상 비어 있을 거라고 가정하고 코드에서 그 슬라이스를 다시 사용하면, 찾기 어려운 버그를 만날 수 있어요.

유일한 예외는 함수 문서가 오류가 발생한 경우에도 다른 반환 값이 의미가 있다고 분명히 밝히는 경우예요. 예시는 [버퍼의 Write 문서][buffer-write]를 참고해요.

오류는 기본적으로 스택 트레이스를 포함하지 않으므로, 오류 메시지를 가능한 한 구체적으로 작성해야 해요. 관례상 오류 메시지는 소문자로 시작하고 마침표로 끝나지 않아야 해요.

이런 오류를 여러 곳에서 사용하고 싶다면 (또는 패키지 사용자에게 오류를 제공하고 싶다면), errors.New를 그 자리에서 쓰는 대신 오류를 위한 변수를 선언해야 해요. 관례상 변수 이름은 Err나 err로 시작해야 해요 (내보내는지 여부에 따라 달라요). 이런 오류 변수를 흔히 _센티널 오류_라고 불러요.

import "errors"

var ErrNotFound = errors.New("resource was not found")

func DoSomething() error {
  // ...
  return ErrNotFound
}

함수 실행 중에 오류가 없었다는 것을 나타내려면 오류에 nil을 반환해요:

func Foo() (int, error) {
  return 10, nil
}

오류 메시지에 변수 포함하기

여러 변수로 더 복잡한 오류 메시지를 만들어야 한다면, 문자열 서식을 활용할 수 있어요. Go에는 이를 위한 특별한 fmt.Errorf 함수가 있어요. 이 함수는 오류 메시지 문자열을 만들어서 오류를 반환해요.

input := 123
action := "UPDATE"

err := fmt.Errorf("invalid input %d for action %s", input, action)
err.Error()
// => "invalid input 123 for action UPDATE"

오류 확인

오류를 반환하는 함수를 호출할 때는 오류 값을 err라는 변수에 저장하는 것이 일반적이에요. 함수의 실제 결과를 사용하기 전에 오류가 없었는지 확인해야 해요. ==와 !=로 오류를 nil과 비교할 수 있고, err가 nil이 아니면 오류가 있었다는 것을 알 수 있어요.

코드의 "정상 경로"가 중첩되지 않게 하려면, 오류 상황을 먼저 처리해야 해요. 보통은 조기 반환으로 처리하죠. 자세한 내용은 Mat Ryer의 유명한 글을 참고해요.

// Do this:
func myFunc() error {
  file, err := os.Open("./users.csv")
  if err != nil {
    return err
  }
  // do something with file
}

// Not this:
func myFunc() error {
  file, err := os.Open("./users.csv")
  if err != nil {
    // handle err
  } else {
    // do something with file
  }
}

// Also not this:
func myFunc() error {
  file, err := os.Open("./users.csv")
  if err == nil {
    // do something with file
  }
  // handle err
}

대부분의 경우, 위 첫 번째 예시처럼 오류는 함수 스택을 따라 위로 반환돼요. 오류를 처리하는 또 다른 방법은 오류를 로그로 남기고 다른 작업을 계속하는 것이에요. 오류를 반환하거나 로그로 남기되, 둘 다 하지는 않는 것이 좋은 관행이에요.

Go의 대부분 함수가 반환 값 중 하나로 오류를 포함하기 때문에, Go 코드 어디에서나 if err != nil 패턴을 보게 되고 사용하게 돼요.

사용자 정의 오류 타입

오류가 단순한 오류 메시지 문자열보다 더 많은 정보를 담기를 원한다면, 사용자 정의 오류 타입을 만들 수 있어요. 앞서 말했듯이, error 인터페이스를 구현하는 것(즉, Error() string 메서드를 가진 것)은 무엇이든 Go에서 오류로 쓸 수 있어요.

보통 사용자 정의 오류 타입을 만들 때는 구조체를 사용해요. 관례상 사용자 정의 오류 타입 이름은 Error로 끝나야 해요. 또한 Error() string 메서드는 포인터 리시버로 정의하는 것이 가장 좋아요. 그 이유는 이 Stackoverflow 댓글에서 확인할 수 있어요. 이 말은 사용자 정의 오류에 대한 포인터를 반환해야 한다는 뜻이에요. 그렇지 않으면 포인터가 아닌 값은 Error() string 메서드를 제공하지 않기 때문에 error로 인정되지 않아요.

type MyCustomError struct {
  message string
  details string
}

func (e *MyCustomError) Error() string {
  return fmt.Sprintf("%s, details: %s", e.message, e.details)
}

func someFunction() error {
  // ...
  return &MyCustomError{
    message: "...",
    details: "...",
  }
}
GitHub에서 편집 링크가 새 창이나 탭에서 열려요

오류 배우기

연습이 잠겨 있어요

오류 개념을 연습하려면 연습 문제 4개를 더 잠금 해제해요