에러 핸들링
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")
}