메뉴

문서정보

목차

Go 언어에서의 error

Go로 프로그램을 개발하다보면, 내장된 error타입을 이용해서 에러를 처리하게 된다. 이 타입은 에러의 상태를 알려주기 위해서 사용한다. os.Open 함수의 경우 파일 열기에 실패했다면 nil이 아닌 error 값을 반환한다. os.Open의 선언을 보자.
func Open(name string) (*File, error)
파일 디스크립터인 File, error 타입을 반환하는 걸 확인 할 수 있다. 실제 프로그램에서는 아래와 같이 사용한다.
f, err := os.Open("filename.txt")
if err != nil {
	log.Println(err.Error())
	return
}
// 파일처리

Error 인터페이스와 errorString 구조체

error는 인터페이스 타입으로 언어에서 직접 지원한다.
type error interface {
	Error() string
}
error는 Error이라는 하나의 메서드만 선언되 있다. 이 메서드는 에러 정보를 담고 있는 string을 반환한다. 따라서 Error()메서드만 구현하는 것 만으로 모든 코드에서의 사용이 가능하다.

Go는 표준 라이브러리 형식으로 errorString 구조체 타입을 제공한다. 이 구조체는 errors 패키지에 선언돼 있다.
type errorString struct {
	s string
}
errorString는 string 타입의 s 필드만 가지고 있다. 이 필드는 unexported 이므로 바로 접근 할 수 없다. Error() 메서드를 이용해서 s값을 가져올 수 있다.
func (e *errorString) Error() string {
    return e.s
}

Error 값만들기

errors 패키지의 New함수를 이용해서 errorString 포인터를 만들 수 있다.
import "errors"
var ErrInvalidParam = errors.New("Mypackage: invalid parameter")
New 함수를 호출하면 아래와 같이 errorString에 값을 설정하고, 포인터를 반환한다.
func New(text string) error {
	return &errorString(text)
}

fmt.Errorf를 이용하면, 형식화된 에러메시지를 만들 수 있다.
var ErrInvalidParam = fmt.Errorf("Invalid parameter [%s]", param)

Error 값 비교하기

bufio와 같은 표준 라이브러리에 있는 패키지를 보면, errors를 이용해서 에러들을 정의한 것을 확인 할 수 있다.
var {
    ErrInvalidUnreadByte = errors.New("bufio: invalid use of UnreadByte")
    ErrInvalidUnreadRune = errors.New("bufio: invalid use of UnreadRune")
    ErrBufferFull        = errors.New("bufio: buffer full") 
    ErrNegativeCount     = errors.New("bufio: nagative count") 
}
bufio 패키지의 메서드들은 에러가 발생했을 때, 위에 정의된 error 들을 반환한다. bufio 메서드를 호출한 코드에서는 위에 선언된 값들을 직접 비교하는 것으로 에러를 분기 할 수 있다.
data, err := b.Peek(1)
if err != nil {
    switch err {
    case bufio.ErrNegativeCount:
        // 에러처리 코드 
        return
    case bufio.ErrBufferFull:
        // 에러처리 코드 
        return
    default:
        // 일반적인 에러처리 코드
        return
	}
}
위 예제를 보자. Peek 메서드는 bufio.Reader 포인터를 반환한다. 어떤 이유로 실패 할 경우, err을 nil이 아닌 값으로 설정한다. 코드에서는 ErrNegativeCount 등의 error와 비교해서 에러의 종류를 확인 할 수 있다.

Error wrapping

애플리케이션을 개발하려면 표준화된 에러 관리 시스템을 만들어야 한다. 아래와 같이 컨텍스트를 추가 할 수 있는 struct를 만들어서 에러 처리 프로세스를 단순화 할 수 있다.
package main

import "fmt"

type UnauthorizedError struct {
    UserId        int
    OriginalError error
}

func (httpErr *UnauthorizedError) Error() string {
    return fmt.Sprintf("User unauthorizedError: %v", httpErr.OriginalError)
}

func validator(userId int) error {
    err := fmt.Errorf("Session invalid for user id %d", userId)
    return &UnauthorizedError{UserId: userId, OriginalError: err}
}

func main() {
    err := validator(1)
    if err != nil {
        fmt.Println(err.Error())
    } else {
        fmt.Println("Valid User")
    }
}

예제에서 애플리케이션 에러를 처리하기 위해서 UserId와 OriginalError 필드를 포함하는 UnauthorizedError 구조체를 만들었다. fmt.Errorf 메서드를 이용해서 시스템 에러 정보를 포함한 error를 반환한다.

main 함수내에서는 error를 처리하는 것과 동일한 방식으로 에러를 처리 할 수 있다. error structure에 추가된 컨텍스트는 디버깅 목적으로만 사용한다. 디버깅 목적으로 사용하고 싶다면 type assertion 처리를 해야 한다. 위 코드를 디버깅 목적으로 수정했다.
package main

import "fmt"

type UnauthorizedError struct {
    UserId        int
    OriginalError error
}

func (httpErr *UnauthorizedError) Error() string {
    return fmt.Sprintf("User unauthorizedError: %v", httpErr.OriginalError)
}

func validator(userId int) error {
    err := fmt.Errorf("Session invalid for user id %d", userId)
    return &UnauthorizedError{UserId: userId, OriginalError: err}
}

func main() {
    err := validator(1)
    if err != nil {
        if errVal, ok := err.(*UnauthorizedError); ok {
            fmt.Println(errVal.UserId)
            fmt.Println(errVal.OriginalError.Error())
        }
    } else {
        fmt.Println("Valid User")
    }
}

type assertion(타입 단언)은 프로그래머가 수동으로 컴파일러에게 특정 변수에 대한 타입 힌트를 주는 것이다.
package main

import "fmt"

func main() {
    var i interface{} = 1
    s, ok := i.(string)
    fmt.Println(s, ok)
}
i.(int)를 사용하면 false가 반환되는 걸 확인 할 수 있을 것이다.

좀 더 현실적인 예제를 하나 만들어보자. REST API 기반의 HTTP 웹 서버를 만들고 있다. HTTP Status Code에 따라서 적당한 메세지를 출력하는 코드를 만들었다.
package main

import "fmt"
import "net/http"

type HttpError struct {
    status int
    method string
}

func (h *HttpError) Error() string {
    return fmt.Sprintf("Something went wrong with the %v request. Server returned %v status.", h.method, h.status)
}

func GetUserEmail(userId int) (string, error) {
    switch userId {
    case 1:
        return "", &HttpError{http.StatusForbidden, "GET"}
    case 2:
        return "", &HttpError{http.StatusNotFound, "GET"}
    case 3:
        return "example@gmail.com", nil
    default:
        return "", &HttpError{http.StatusBadRequest, "GET"}
    }
}

func main() {
    for _, userId := range []int{1, 2, 3} {
        if email, err := GetUserEmail(userId); err != nil {
            fmt.Println(err)
        } else {
            fmt.Println("User email is", email)
        }
    }
}
먼저 상태정보와 메서드 정보를 담기 위한 필드를 포함한 HttpError 구조체를 만들었다. 그리고 Error() 메서드를 구현해서 오류 인터페이스를 구현한다. 이 에러메시지는 상태값과 메서드같은 상세정보를 리턴한다.

GetUserEmail은 HTTP 유저 아이디에 대한 이메일 정보를 리턴하는 모의 함수다. userId가 1일때는 StatusForbidden, 2일때는 StatusNotFound ... 를 리턴하게했다.

main 함수에서 GetUserEmail 함수를 호출하면 유저의 email과 err를 반환한다. err은 error의 인터페이스다. 만약 에러가 StatusForbidden 이면, 세션정리를 하는 작업을 수행하도록 했다.

type switch를 이용해서 다른 방식으로 구현 할 수도 있다. 아래 코드를 보자.
package main

import (
    "fmt"
)

type NetworkError struct{}

func (n *NetworkError) Error() string {
    return "A network connection was aborted"
}

type FileSaveFailedError struct{}

func (f *FileSaveFailedError) Error() string {
    return "The requested file could not be saved."
}

func saveFileToRemote() error {
    result := 2
    if result == 1 {
        return &NetworkError{}
    } else if result == 2 {
        return &FileSaveFailedError{}
    } else {
        return nil
    }
}

func main() {
    switch err := saveFileToRemote(); err.(type) {
    case nil:
        fmt.Println("File successfully saved.")
    case *NetworkError:
        fmt.Println(err)
    case *FileSaveFailedError:
        fmt.Println(err)
    }
}

네트워크 에러와 파일 에러메시지를 위해서 NetworkErrorFileSaveFailedError 구조체를 마들고 error 인터페이스를 위한 Error() 메서드를 만들었다. saveFileToRemote 함수는 에러에 맞는 structure를 리턴한다.

main 함수에서 saveFileToRemote 함수를 호출하면 error를 리턴한다. 이제 error의 타입을 확인해서 switch로 분기처리 하면 된다.

사용자 정의 error interface

임베디드(embeded) 인터페이스를 이용하면 여러 인터페이스를 병합한 인터페이스를 만들 수 있다. 이 원리를 이용하면 오류 인터페이스와 추가적인 메서드를 포함한 인터페이스를 만들 수 있다. 이 인터페이스는 에러 메서드와 (주로 에러 분석을 위한)여러 가지 메서드들을 포함 할 수 있다.
package main

import (
	"fmt"
)

// 인증 실패 테스트를 위한 user id
var (
	UnauthorizedId = 187868884
)

// 유저 세션 정보 구조체
type UserSessionState interface {
	error
	isLoggedIn() bool
	getSessionId() int
}

// 인증 실패 정보를 담고 있는 구조체
type UnauthorizedError struct {
	UserId    int
	SessionId int
}

func (err *UnauthorizedError) isLoggedIn() bool {
	return err.SessionId != 0
}

func (err *UnauthorizedError) getSessionId() int {
	return err.SessionId
}

func (err *UnauthorizedError) Error() string {
	return fmt.Sprintf("User with id %v unauthorized to perform this action", err.UserId)
}

func validateUser(userId int) error {
	switch userId {
	case UnauthorizedId:
		return &UnauthorizedError{userId, UnauthorizedId}
	default:
		return nil
	}
}

func main() {
	for _, userId := range []int{UnauthorizedId, 4} {
		if err := validateUser(userId); err != nil {
			if errVal, ok := err.(UserSessionState); ok {
				if errVal.isLoggedIn() {
					fmt.Printf("Cleaning user session with session id %v\n", errVal.getSessionId())
				}
			}
		} else {
			fmt.Println("Authorized User", userId)
		}
	}
}
약간 혼동될 수 있는데(인터페이스 들어가면 혼동되기 마련이다.) 간단하다. isLoggineIn 메서드와 getSessionId 메서드를 포함하는 UserSessionState 인터페이스가 있다. 그리고 error 인터페이스도 가지고 있다. 따라서 UserSessionState는 error를 표현하기 위한 타입으로 사용 할 수 있다.

UnauthorizedError 구조체는 getSessionId와 isLoggedIn 메서드 Error 메서드를 모두 구현하고 있기 때문에, error와 UserSessionState 인터페이스를 모두 구현하고 있다. 아래 그림은 이 구조를 묘사하고 있다.

 Multi interface

에러 핸들링 팁

Go는 다른 객체지향 언어에서 사용하는 try/cache문을 제공하지 않는다. Go는 C언어처럼 에러가 발생한 지점에서 에러를 처리한다. 이러한 에러처리가 익숙하지 않을 수 있는데, go에서 에러를 잘 처리하기 위한 몇 가지 지침이 있으니 참고하자.

에러 무시

blank identifier를 이용해서 에러를 무시할 수 있다.
val, _ := someFunctionWhichCanReturnAnError();
간혹 에러를 무시하는게 더 나을 수도 있다. 물론 일반적인 상황에서 에러를 무시하는 건 좋은 선택은 아니다.

에러만 반환하는 건 좋은 생각은 아니다.

func someFunction() error {
  val, err := someFunctionWhichCanReturnAnError();  if err != nil {
    return err
  } else {
    // proceed further
  }
}
someFunction()은 에러만 반환하고 있는데, 에러를 추적하기가 쉽지 않기 때문에 좋은 생각이 아니다. wrapper 함수를 만들어서 사용하자.

문제가 발생한 지점을 정확히 파악하고 싶다면, 에러가 발생한 코드의 정보를 로그메시지에 포함하는 것도 생각해보자.
package main  
  
import (  
    "fmt"  
    "runtime"  
)  
  
type fileError struct {  
    code int  
    error  
}  
  
func (err *fileError) Error() string {  
    return fmt.Sprintf("File Open Error %d", err.code)  
}  
  
func someFunction() error {  
    _, fn, line, _ := runtime.Caller(1)  
    fmt.Println(fn, line)  
    return &fileError{code: 101}  
}  
   
func main() {  
    err := someFunction()  
    if err != nil {  
        fmt.Println(err.Error())  
    }  
}      

한번에 여러 개의 에러를 다루지 말자

함수가 함수를 호출 하는 경우 한 번에 여러 개의 에러를 처리해야 할 수 있다. 아래 예제를 보자.
func someFunction() error {
  if err != nil {
    // log error
    // return err
  }
}

func someOtherFunction() error {
  val, err := someFunction()
  if err != nil {
    // log error
    // return err
  }
}
위의 예제에서는 동일한 에러를 두번 기록한다. 함수는 중첩해서 호출할 수 있기 때문에 같은 에러가 여러번 로깅 될 수 있다. 중복된 정보는 좋지 않다. 따라서 로그를 기록할 적당한 위치를 설정하는 것도 중요하다. 일반적으로 정렬 함수와 같은 유틸리티 함수는 에러를 처리하지 않는다. 에러에 대한 처리는 호출하는 쪽의 책임이다.