Skip to main content

에러 핸들링

Go의 오류 처리는 자바, 자바스크립트, 파이썬과 같은 다른 주류 프로그래밍 언어와 약간 다릅니다. Go의 기본 제공 에러 패키지는 스택 추적을 포함하지 않고 에러를 처리하기 위한 try/catch도 지원하지 않습니다. 대신 Go에서 에러는 함수가 반환하는 값일뿐이며, 다른 데이터 유형과 거의 동일한 방식으로 처리할 수 있어 놀라울 정도로 가볍고 단순한 설계가 가능합니다.

type error interface {
Error() string
}

Go에서는 위와 같이 에러 타입이 구현되어 있고 기본적으로 오류 메시지를 문자열로 반환하는 Error() 메서드를 구현하는 것을 의미합니다.

오류는 Go의 기본 제공 패키지 errors 또는 fmt 패키지를 사용해서 즉석에서 생성할 수 있습니다. 예를 들어, 아래 함수는 오류 패키지를 사용하여 정적 오류 메시지와 함께 새 오류를 반환합니다.

package main

import "errors"

func DoSomething() error {
return errors.New("something didn't work")
}

fmt 패키지를 사용해서 int, 문자열 또는 다른 에러와 같은 동적 데이터를 오류에 추가할 수 있습니다.

package main

import "fmt"

func Divide(a, b int) (int, error) {
if b == 0 {
return 0, fmt.Errorf("can't divide '%d' by zero", a)
}
return a / b, nil
}

오류는 nil로 반환될 수 있지만 실제로 Go에서 오류 발생 시 기본값인 "0"이 반환됩니다. 이는 오류가 발생했는지 여부를 확인하는 관용적인 방법(try/catch 문 대체)으로 중요합니다.

오류는 일반적으로 함수의 마지막 인수로 반환되며 오류 메시지는 일반적으로 소문자로 작성되며 문장 부호로 끝나지 않지만 고유명사, 대문자로 시작하는 함수 이름 등을 포함하는 경우에는 예외가 될 수 있습니다.

Go에서는 명시적으로 오류를 확인할 수 있도록 예상되는 오류를 미리 정의하여 사용할 수 있으며 이러한 방식은 오류 발생에 따른 코드 분기에 유용합니다.

package main

import (
"errors"
"fmt"
)

var ErrDivideByZero = errors.New("divide by zero")

func Divide(a, b int) (int, error) {
if b == 0 {
return 0, ErrDivideByZero
}

return a / b, nil
}

func main() {
a, b := 10, 0
result, err := Divide(a, b)
if err != nil {
switch {
case errors.Is(err, ErrDivideByZero):
fmt.Println("divide by zero error")
default:
fmt.Printf("unexpected division error: %s\n", err)
}
}

fmt.Printf("%d / %d = %d\n", a, b, result)
}

위 방식으로 많은 오류를 핸들링할 수 있지만 조금 더 많은 기능을 원할 때가 있는데 이때는 오류에 추가 데이터 필드를 포함하거나 오류 메시지가 인쇄될 때 동적 값으로 채우는 것입니다.

package main

import (
"errors"
"fmt"
)

type DivisionError struct {
IntA int
IntB int
Msg string
}

func (e *DivisionError) Error() string {
return e.Msg
}

func Divide(a, b int) (int, error) {
if b == 0 {
return 0, &DivisionError{
Msg: fmt.Sprintf("cannot divide '%d' by zero", a),
IntA: a, IntB: b,
}
}
return a / b, nil
}

func main() {
a, b := 10, 0
result, err := Divide(a, b)
if err != nil {
var divErr *DivisionError
switch {
case errors.As(err, &divErr):
fmt.Printf("%d / %d is not mathematically valid: %s\n",
divErr.IntA, divErr.IntB, divErr.Error())
default:
fmt.Printf("unexpected division error: %s\n", err)
}
return
}

fmt.Printf("%d / %d = %d\n", a, b, result)
}

2019년에 Go 1.13이 출시되기 전에는 표준 라이브러리에 오류 작업을 위한 API가 많지 않았으며, 기본적으로 errors.New와 fmt.Errorf만 있었지만 Go 1.13이 출시되면서 특정 오류 유형을 검사하는 데 유용한 errors.Wrap 및 errors.Unwrap을 비롯한 몇 가지 새로운 오류 API가 도입되었습니다.

오류 래핑하기 전 코드

  • users/db.go
package db

type User struct {
ID string
Username string
Age int
}

func FindUser(username string) (*User, error) { /* ... */ }
func SetUserAge(user *User, age int) error { /* ... */ }
  • main.go
package main

import (
"errors"
"fmt"

"test/users/db"
)

func FindUser(username string) (*db.User, error) {
return db.Find(username)
}

func SetUserAge(u *db.User, age int) error {
return db.SetAge(u, age)
}

func FindAndSetUserAge(username string, age int) error {
var user *User
var err error

user, err = FindUser(username)
if err != nil {
return err
}

if err = SetUserAge(user, age); err != nil {
return err
}

return nil
}

func main() {
if err := FindAndSetUserAge("bob@example.com", 21); err != nil {
fmt.Println("failed finding or updating user: %s", err)
return
}

fmt.Println("successfully updated user's age")
}

만약 데이터베이스 작업 중 잘못된 요청 오류로 에러가 발생하면 메인 함수는 아래와 같이 에러 내용을 출력할 것입니다.

failed finding or updating user: malformed request

하지만 이 에러만 보고서 정보가 충분하지 않기 때문에 어느 작업에서 오류가 발생했는지 알 수 없습니다. Go 1.13에서는 어떤 함수에서 발생한 에러인지 정보를 추가하여 추적에 용이하게 합니다.

package main

import (
"errors"
"fmt"

"test/users/db"
)

func FindUser(username string) (*db.User, error) {
u, err := db.Find(username)
if err != nil {
return nil, fmt.Errorf("FindUser: failed executing db query: %w", err)
}
return u, nil
}

func SetUserAge(u *db.User, age int) error {
if err := db.SetAge(u, age); err != nil {
return fmt.Errorf("SetUserAge: failed executing db update: %w", err)
}
}

func FindAndSetUserAge(username string, age int) error {
var user *User
var err error

user, err = FindUser(username)
if err != nil {
return fmt.Errorf("FindAndSetUserAge: %w", err)
}

if err = SetUserAge(user, age); err != nil {
return fmt.Errorf("FindAndSetUserAge: %w", err)
}

return nil
}

func main() {
if err := FindAndSetUserAge("bob@example.com", 21); err != nil {
fmt.Println("failed finding or updating user: %s", err)
return
}

fmt.Println("successfully updated user's age")
}