Ultimate-Go-06 [에러처리]
대부분의 고 패키지 또는 라이브러리를 보게 되면 결괏값과 error 인터페이스를 주로 반환되는 것을 상당 부분 많이 확인할 수 있다.
실제 이러한 에러처리가 미숙해 한번 서버를 패닉으로 내려버린 적도 있다.
이에 대해 고의 에러처리 철학과 그 방법에 대해 알아보자.
기본에러 값
type error interface {
Error() string
}
고에서 빌트인으로 제공하는 에러 인터페이스이다.
해당 error 패키지 에서 New를 이용해 error 포인터 구조체를 반환한다.
(error는 unexport 타입인데 어떻게 패키지에서 import 가 가능할까?- 빌트인 함수이기에 가능)
반환된 구조체는 errorString error 인터페이스를 만족하는 구현체가 있고 Error는 단순하게 메시지를 반환한다.
이렇게 에러메세지는 디커플링 되어있다. 왜? 클라이언트 즉 error를 구현하고 받는 모든 의사소통은
인터페이스로 이루어주기 때문이다.
가장 기본적인 에러 핸들링방식
func TestError01(t *testing.T) {
webCall := func() error {
n := time.Now().Unix()
if n%2 == 0 {
return nil
}
return errors.New("지금은 홀수 입니다.")
}
if err := webCall(); err != nil {
fmt.Println(err)
panic(err)
}
fmt.Println("Life is Good")
}
함수를 호출하고 반환되는 함수를 처리한다.
에러변수
위처럼 인터페이스를 만족만 시킨다면? 정말 손쉽게 커스텀 에러작성이 가능해진다.
gorm에 정의되어 있는 커스텀 에러를 확인해 보면
var (
// ErrInvalidTransaction invalid transaction when you are trying to `Commit` or `Rollback`
ErrInvalidTransaction = errors.New("invalid transaction")
--- 생략 ---
)
이런 식으로 errors 패키지에서 제공해 주는 생성자함수를 이용하면 손쉽게 에러생성이 가능하다.
func TestError02(t *testing.T){
--- 생략 webCall() ---
if err := webCall(); err != nil {
switch err {
case ErrBadRequest:
fmt.Println("Bad Request")
return
case ErrBadPageMoved:
fmt.Println("The Page Moved")
return
}
}
}
타입 컨택스트
빌트인에서 제공되는 에러 인터페이스보다 더많은 컨택스트가 필요한 경우도 존재한다.
예를 들어 네트워킹 문제는 복잡할 수 있어, 에러 변수 만으로는 부족하다. 이럴 때 사용자 정의 타입을 통해 문제를 해결할 수 있다.
json 패키지중 unmarshal error 가 그중 하나이다.
type InvalidUnmarshalError struct {
Type reflect.Type
}
func (e *InvalidUnmarshalError) Error() string {
if e.Type == nil {
return "json: Unmarshal(nil)"
}
if e.Type.Kind() != reflect.Pointer {
return "json: Unmarshal(non-pointer " + e.Type.String() + ")"
}
return "json: Unmarshal(nil " + e.Type.String() + ")"
}
json 값을 언마샬링 할때 포인터의 값이 들어오지 않을 경우 반환값으로 해당 구조체가 사용된다.
func TestError03(t *testing.T) {
type user struct {
Name string
}
var u user
err := json.Unmarshal([]byte(`{"name":"bill"}`), u)
switch e := err.(type) {
case *json.InvalidUnmarshalError:
fmt.Printf("Invalid input type %v\n", e.Type)
default:
fmt.Println(err)
}
fmt.Println("Name", u.Name)
}
기능을 통한 컨택스트 처리
위에서 제공된 방법은 타입을 꺼내와서 사용하기에 디커플링으로부터 멀어진다. 디커플리를 유지하면서 타입단언을 막는 방법에 대해 알아보자.
type CustomOneError struct {
message string
}
func (c *CustomOneError) Error() string {
return "one error" + c.message
}
type CustomTwoError struct {
message string
}
func (c *CustomTwoError) Error() string {
return "two error" + c.message
}
func TestError04(t *testing.T) {
errChecker := func() error {
n := time.Now().Unix()
if n%2 == 0 {
return &CustomOneError{"wrong"}
} else {
return &CustomTwoError{"something"}
}
}
err := errChecker()
switch e := err.(type) {
case *CustomOneError:
if e.IsNotInteger(){
fmt.Println("Not Integer",e.message)
}
case *CustomTwoError:
if e.IsNotInteger(){
fmt.Println("Not Integer",e.message)
}
}
}
이렇게 숫자가 아닌경우의 에러를 핸들링하기 위해서 swtich-case로 위에서 제공되는 방법으로 사용하는 경우이다.
숫자가 들어오는 경우에 대해서만 신경을 쓴다고 가정한다면 해당 switch는 인터페이스로 포장할 수 있다.
func TestError04(t *testing.T) {
--- 생략 ---
switch e := err.(type) {
case ErrNotInteger:
fmt.Println("Not Integer Error ", e.IsNotInteger())
default:
fmt.Println("default ", err)
}
}
에러포장
github.com/pkg/errors.Wrap() 해당 패키지를 이용하면 고에서 제공하는 일반 렙핑이 아닌 스택트레이스가 제공되는 에러를 반환할 수 있다.
func TestError05(t *testing.T) {
thirdCall := func(i int) error {
return errors.New("99")
}
secondCall := func(i int) error {
if err := thirdCall(i); err != nil {
return fmt.Errorf()
}
return nil
}
firstCall := func(i int) error {
if err := secondCall(i); err != nil {
return pkgErr.Wrap(err, "Second Call Error")
}
return nil
}
for i := 0; i < 5; i++ {
if err := firstCall(i); err != nil {
fmt.Println("stack trace")
fmt.Printf("%+v\n", err)
}
}
}
//고 기본 패지키이용 방법
return fmt.Errorf("third call error %w", err)
기본 패키지를 이용해서 랩핑이 가능하지만, 스택트레이스 없이 에러로그를 확인하는것은 상단 한 피로감을 유발할 것으로 보인다.
이렇게 처리된 에러는 에러의 근원지를 확인할수 있는. Cause method를 제공한다.
func TestError05(t *testing.T) {
--- 생략 ---
for i := 0; i < 1; i++ {
if err := firstCall(i); err != nil {
switch v:= pkgErr.Cause(err).(type){
case *ErrCustomOne:
fmt.Println("custom One error")
}
--- 생략 ---
}
}
}
이 방법외에도 errors 기본패키지에서 as 메서드를 제공한다.
func TestError05(t *testing.T) {
thirdCall := func(i int) error {
return &CustomOneError{"hoit"}
}
secondCall := func(i int) error {
if err := thirdCall(i); err != nil {
return fmt.Errorf("third call error %w", err)
}
return nil
}
firstCall := func(i int) error {
if err := secondCall(i); err != nil {
return fmt.Errorf("second call error %w", err)
}
return nil
}
for i := 0; i < 1; i++ {
if err := firstCall(i); err != nil {
var v *CustomOneError
switch {
case errors.As(err, &v):
fmt.Println("custom One error found")
}
fmt.Println("Stack Trace")
fmt.Printf("%+v\n", err)
fmt.Printf("Stack Done")
}
}
}
as를 사용하면 기본 빌트인 타입으로도 충분히 에러 발생 근원지에 대해 트랙킹 할 수 있다.
이렇게 다양한 error를 go 에서는 지원해주고 있다.
왜? 명시적으로 err를 처리하기를 바라기 때문이다. 다시 말해 err 처리를 회피하지 않고 제대로 처리하길 바라는 마음에서 위와 같은 다양한 빌트인 함수를 제공해 주고, 또 계속 패키지를 발전시켜 나가는 것이 아닌가 싶다.
출처: https://github.com/hoanhan101/ultimate-go/tree/master/go/design