error 인터페이스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")
}
오류가 있을 때 모든 함수가 다른 반환 값에 제로 값을 반환한다고 가정하면 안 돼요. 오류가 발생했다면 다른 반환 값 중 어느 것도 안전하게 사용할 수 없다고 가정하는 게 좋아요. 유일한 예외는 문서에서 오류가 발생한 경우에도 다른 반환 값이 의미 있다고 명확히 밝히는 경우예요.
이런 간단한 오류를 여러 곳에서 사용하고 싶다면, 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
}
오류를 반환하는 함수를 호출한다면, 오류 값을 err라는 변수에 저장하는 게 일반적이에요. 함수의 실제 결과를 사용하기 전에 오류가 없었는지 확인해야 해요.
코드의 "정상 경로"가 중첩되지 않도록, 오류 상황을 먼저 처리하는 게 좋아요. ==와 !=로 오류를 nil과 비교할 수 있고, err가 nil이 아니면 오류가 발생했다는 것을 알 수 있어요.
func processUserFile() error {
file, err := os.Open("./users.csv")
if err != nil {
return err
}
// do something with file
}
대부분의 경우 위 예시처럼 오류는 함수 스택을 따라 위로 반환돼요. 오류를 처리하는 또 다른 방법은 오류를 로그로 남기고 다른 작업을 계속하는 거예요. 오류는 반환하거나 로그로 남기되, 둘 다 하는 건 좋지 않아요.
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: "...",
}
}
오래 기다려 온 그날이 마침내 찾아왔어요. 이제 알프스에 있는 아름다운 농장의 자랑스러운 주인이 되었어요.
그래도 아침 일찍 일어나 소에게 먹이를 주는 건 여전히 싫어요.
뛰어난 엔지니어답게 직접 사료 급여기를 만들기로 해요. 이름은 FEED-M-ALL이에요.
프로젝트를 끝내기 위해 마지막으로 필요한 것은 각 소가 받아야 할 사료의 양을 계산하는 코드예요. 모든 소가 같은 양을 받는 게 중요해요. 소들 사이에 다툼이 생기지 않게 해야 해요. 소는 아주 예민하거든요.
다행히 사료 양을 계산하는 공식을 직접 만들어 낼 필요는 없어요. 인터넷에서 찾은 정체불명의 외부 라이브러리를 사용하면 되거든요. 이 라이브러리를 쓰면 소들이 가장 행복해진다고 해요. 이 라이브러리는 다음 인터페이스를 만족하는 타입을 제공해요. 직접 작성할 코드에서 이 인터페이스에 의존하게 될 거예요.
type FodderCalculator interface {
FodderAmount(int) (float64, error)
FatteningFactor() (float64, error)
}
코드를 작성하면서 오류 처리를 개선하게 돼요. 그러면 나중에 농장 일상에서 사용할 때 코드가 더 견고해지고 디버깅하기도 쉬워져요.
먼저 소 한 마리당 사료의 양을 계산하는 코드를 작성하는 데 집중해요.
FodderCalculator와 소의 수를 정수로 인자로 받는 함수 DivideFood를 구현해요.
이 과제에서는 전달되는 소의 수가 항상 0보다 크다고 가정해요.
이 함수는 소 한 마리당 먹이의 양을 float64로 반환하고, 오류가 발생하면 오류를 반환해야 해요.
계산을 하려면 먼저 모든 소에게 필요한 사료의 총량을 가져와야 해요.
FodderAmount 메서드를 호출하면서 소의 수를 전달하면 돼요.
그리고 이 양에 곱해야 하는 계수도 필요해요.
이 계수는 FatteningFactor 메서드를 호출해서 얻어요.
이 두 값과 소의 수를 이용하면 소 한 마리당 먹이의 양을 (float64로) 계산할 수 있어요.
이 값을 DivideFood 함수에서 반환하면 돼요.
호출한 메서드 중 하나가 오류를 반환하면 실행을 멈추고, 그 오류를 (그대로) DivideFood 함수에서 반환해야 해요.
// For this example, we assume FodderAmount returns 50
// and FatteningFactor returns 1.5.
DivideFood(fodderCalculator, 5)
// => 15 <nil>
// Now assuming FodderAmount returns an error with message "something went wrong".
DivideFood(fodderCalculator, 5)
// => 0 "something went wrong"
위의 첫 번째 과제를 하다 보면, 사용하는 외부 라이브러리가 생각만큼 품질이 좋지 않다는 걸 깨닫게 돼요. 예를 들어, 잘못된 입력을 제대로 처리하지 못해요. 이 한계를 피하려면 직접 작성하는 코드에서 입력값을 검사하는 부분을 추가하면 돼요.
위의 DivideFood와 시그니처가 같은 함수 ValidateInputAndDivideFood를 작성해요.
DivideFood를 호출하고 그 결과를 반환해야 해요."invalid number of cows"라는 메시지를 담은 오류를 반환해야 해요.ValidateInputAndDivideFood(fodderCalculator, 5)
// => 15 <nil>
ValidateInputAndDivideFood(fodderCalculator, -2)
// => 0 "invalid number of cows"
소의 수를 먼저 확인하는 건 좋은 선택이었지만, 구체적이지 않은 오류 메시지는 그다지 마음에 들지 않아요.
더 나은 방법으로 InvalidCowsError라는 사용자 정의 오류 타입을 만들기로 해요.
이 사용자 정의 오류는 소의 수(int)와 사용자 정의 메시지(string)를 담고 있어야 하고, Error 메서드는 데이터를 다음 형식으로 직렬화해야 해요:
{number of cows} cows are invalid: {custom message}
사용자 정의 오류를 갖추었으니, 소의 수를 정수로 받고 오류(또는 nil)를 반환하는 함수 ValidateNumberOfCows를 구현해요.
"there are no negative cows"로 설정된 InvalidCowsError를 반환해요."no cows don't need food"로 설정된 InvalidCowsError를 반환해요.nil을 반환해요.err := ValidateNumberOfCows(-5)
err.Error()
// => "-5 cows are invalid: there are no negative cows"
이 검증 함수를 만드는 고된 작업을 끝내고 나면, 어느새 저녁이 된 걸 알아차리고 책상을 떠나 산 위로 지는 노을을 즐기러 가요. 새 검증 함수를 실제로 코드에 추가하는 일은 다음 날로 미뤄둬요.
Exercism에 가입하고 Go 트랙을 개념 34개연습 문제 166개, 그리고 실제 사람의 멘토링과 함께 배우고 익혀 보세요. 모두 무료예요.