Go 에러 핸들링 패턴 정리

배경

Node.js로 작성된 API 서버의 성능 이슈로 Go 1.10으로 재작성을 진행했다. 가장 적응이 어려웠던 부분은 에러 핸들링이었다.

기본 패턴

Go는 예외가 없고 에러를 반환값으로 처리한다.

func fetchUser(id int) (*User, error) {
    user, err := db.Query("SELECT * FROM users WHERE id = ?", id)
    if err != nil {
        return nil, err
    }
    return user, nil
}

처음엔 if err != nil 반복이 장황하게 느껴졌지만, 에러가 발생할 수 있는 지점이 명확해지는 장점이 있었다.

에러 래핑

에러 컨텍스트를 추가할 때는 fmt.Errorf를 사용했다.

user, err := fetchUser(id)
if err != nil {
    return fmt.Errorf("failed to fetch user %d: %v", id, err)
}

커스텀 에러 타입

HTTP 상태 코드가 필요한 경우 커스텀 에러를 정의했다.

type APIError struct {
    StatusCode int
    Message    string
}

func (e *APIError) Error() string {
    return e.Message
}

panic/recover

panic은 정말 복구 불가능한 상황에만 사용했다. 대부분은 명시적으로 에러를 반환하는 방식이 더 안전했다.

소감

처음엔 불편했지만 에러 처리가 강제되면서 예외 처리를 누락하는 경우가 줄었다. Node.js에서 uncaughtException으로 서버가 죽던 경험을 생각하면 이 방식이 더 안정적이다.