
어떤 프로그래밍 언어든 에러를 핸들링하는 것은 중요하다. 오늘은 Go에서 에러의 정의와 핸들링하는 방법을 알아볼 것이다.
에러는 다음과 같은 인터페이스를 갖는다.
type error interface {
Error() string
}
그러니까, 대충 커스텀 에러를 만들어서 Error만 구현해주면 된다는 것이다.
아마 Goland로 Go를 개발해봤을 시 IDE에서 에러 끝에 .을 붙이지 말라는 메세지를 만나본 적이 있을텐데, 이는 Go가 에러를 fragment로 치부하기 때문이다.
Go에서는 Rust에서의 Backtrace 같은 기능이 없다. 그래서 에러를 반환할 때 fmt.Errorf 메소드를 많이 사용한다. (물론 runtime.Stack을 호출할 순 있지만 완벽하진 않다.)
// 미리 정의된 에러
var errFooIsNotEven = errors.New("foo is not even")
func ValidateIsFooEven(foo int) error {
if foo % 2 != 0 {
return fmt.Errorf("validation failed: %w", errFooIsNotEven)
}
return nil
}
// validationo failed: foo is not even
또한, 불변성을 유지한답시고 다음처럼 같이 짜면 안된다.
const errFooIsNotEven = "foo is not even"
// ...
errors.New(errFooIsNotEven)
객체의 재생성 오버헤드나 이런 걸 떠나서, 저렇게 짜면 에러를 비교할 수 없게된다.
에러도 일종의 객체 취급이라 var로 전역에 선언하는걸 추천한다.
이제 어떤식으로 에러를 핸들링하는지 알아보자.
가장 단순하면서도 많이 사용되는 형태이다.
값이랑 같이 받고 err를 검증하는 형식이다.
func sumUntilTen(a int, b int) (int, error) {
result := a + b
if result > 10 {
return 0, errors.New("invalid sum")
}
return 0, nil
}
// main.go
v, err := sumUntilTen(5, 6)
if err != nil {
// 에러 핸들링
}
void 형태로 호출할 때 발생할 수 있는 에러를 반환할 때 많이 쓰인다.
혹은 Go로 웹 프레임워크를 많이 만져봤다면 단일 에러 반환을 많이 봤을 것이다.
var errFoo = errors.New("foo")
func fooMakesError() error {
return errFoo
}
// main.go
err := fooMakesError()
if err != nil {
// 에러 핸들링
}
반복적인 에러 처리(로깅 등등)가 싫다면 이런식으로 처리할 수도 있다.
물론, 가독성은 좋지 않다.
func fooHandlingError() {
var err error
defer func() {
if err != nil {
// 공통 에러 로깅
}
}()
err = fooMakesError()
if err != nil {
// 고유한 에러 핸들링, 혹은 비워둠
return
}
}
얼핏보면 Go의 에러 처리는 간결하고 좋아보일 수 있다. 그렇지만, 당연히 tradeoff를 감수한 것이기에 sucks 소리를 많이 듣는다.

Rust같은 경우는 ? 표현식을 언어단에서 지원하여, 가독성을 높였다. 반면 방금 소개한 Go의 에러 핸들링은 반복적이고, 코드가 길어지며, 에러의 디버깅이 어렵다. 그러니까, 자바로 예시를 들자면 Exception으로 싹 다 짬 때려버리는 것이다.
애초에 요새 트렌드랑 좀 다른 방법이다.
근래에 나온 언어들을 살펴보자. 모나딕한 에러 처리를 지원하는 걸 꽤 중요하게 여긴다. Kotlin, Rust, Swift, 심지어 자바마저 Optional을 지원한다.
물론 사용자가 직접 type wrapping을 해서 구현할 수 있겠지만, 이건 근본적인 해결책이 아니다. 프로젝트가 달라질 때마다 일일이 선언하거나 라이브러리를 가져오다니.. 구시대 C인가?
참조