はじめに
Go1.13から機能追加されたエラーに関してキャッチアップがまだできていないアプリケーション開発者のための記事です。
TL;DR
アプリケーション実装者は以下をすることで嬉しさを得られます。
-
fmt.Errorf("%w", err)
またはUnwrap() error
を実装したエラー型をカスタムエラーにする。 -
errors.Is
関数、errors.As
関数を使いエラーハンドリングの受け口を実装する。
Go1.12までのバージョンとの実装方法の比較はGo 1.13時代のエラー実装者のお作法の記事が非常に参考になります。
環境
$ go version
go version go1.13.1 darwin/amd64
ライブラリが提供するエラーとの付き合い方
ライブラリが返すエラー値をfmt.Errorf("%w", err)
でラップすればerrors.Is
関数で中身のライブラリ側エラー値を判定できる
以下のコードをあるGoのライブラリが提供するものだとします。
package main // 本来は package lib な形式
import (
"errors"
"time"
)
var (
ErrParse = errors.New("lib: parse error")
)
type LibTime time.Time
func ParseLibFunc(text string) (LibTime, error) {
t, err := time.Parse(time.RFC3339, text)
if err != nil {
return LibTime{}, ErrParse // ライブラリが持つエラー型の値を返す
}
return LibTime(t), nil
}
このライブラリの関数ParseLibFunc
を利用するアプリケーションを作っているとします。
ここではservice
というアプリの関数がライブラリ関数を呼び、ライブラリ関数から返ったエラーをGo1.13からのfmt.Errorf("%w", err)
の記法でラップしながらエラーを呼び元であるmain
関数に返します。ここではmain
関数はアプリコードのエラーハンドリングを担当する関数とします。
package main
import (
"errors"
"fmt"
// "XXX/User/lib" // 本来はこのようにライブラリをインポートする
)
func service(text string) error {
_, err := ParseLibFunc(text)
if err != nil {
return fmt.Errorf("service error with '%w'", err) // Unwrapを実装したエラーを返す
}
return nil
}
func main() {
if err := service("2020/02/14"); err != nil {
// errors.Is 関数を使ってライブラリ提供のエラーかを判定
if errors.Is(err, ErrParse) {
// handle error
}
// 以下検証用
fmt.Println(err) // service error with 'lib: parse error'
// errors.Unwrap 関数でライブラリ関数を取り出す()
wrappedErr := errors.Unwrap(err)
fmt.Println(wrappedErr) // lib: parse error
fmt.Println(errors.Is(wrappedErr, ErrParse)) // true
fmt.Println(errors.Is(err, ErrParse)) // true
}
}
service
関数で新たなエラー型の値にしていますがfmt.Errorf("%w", err)
とラップしているので、errors.Is
関数を使うことでライブラリのエラー値の判定が可能になっています。
また、errors.Unwrap
関数によりラップされた1つ分内側のエラー値を得ることができます。errors.Is
関数は内部でerrors.Unwrap
を繰り返し呼び出すことで判定を実現しています。
ライブラリが独自の公開エラー型の値を返すときは、ラップしてもerrors.As
関数で本来のエラー値を得られる
package main // 本来は package lib な形式
import (
"errors"
"time"
)
// LibError はライブラリの独自エラー型
type LibError struct {
kind string
orgError error
}
func (l *LibError) Error() string {
return "error occured in Lib"
}
func (l *LibError) Kind() string {
return l.kind
}
type LibTime time.Time
func ParseLibFunc(text string) (LibTime, error) {
t, err := time.Parse(time.RFC3339, text)
if err != nil {
return LibTime{}, &LibError{kind: "Parse", orgError: err} // 独自エラー型の値を返す
}
return LibTime(t), nil
}
package main
import (
"errors"
"fmt"
)
func service(text string) error {
_, err := ParseLibFunc(text)
if err != nil {
return fmt.Errorf("service error with '%w'", err) // Unwrapを実装したエラーを返す
}
fmt.Println("service finished successfully")
return err
}
func main() {
if err := service("2020/02/14"); err != nil {
var e *LibError
// errors.As 関数を使うことでライブラリの独自エラー型の本来の値を得ることができる
if errors.As(err, e) {
fmt.Println(e) // error occurred in Lib
fmt.Println(e.Kind()) // Parse
}
}
}
errors.As
関数を使うことでライブラリ独自エラー型の値が実装するKind()
メソッドを実行することができています。
アプリコード内のカスタムエラーはUnwrap() error
を実装しよう
上記の例ではservice
関数はfmt.Errorf("%w", err)
でUnwrapが実装されたエラー値を返していましたがアプリ内での独自エラー型を実装する場合はGo1.13時代以降はUnwrapメソッドを実装することが理想になります。
// AppError はアプリコード側の独自エラー型
type AppError struct {
orgErr error
code string
}
func (e AppError) Error() string {
return fmt.Sprintf("code: %s, msg: app error occurred", e.code)
}
func (e AppError) Unwrap() error {
return e.orgErr
}
func service(text string) error {
_, err := ParseLibFunc(text)
if err != nil {
return AppError{orgErr: err, code: "00A"}
}
return err
}
func main() {
if err := service("2020/02/14"); err != nil {
fmt.Printf("%T\n", err) // main.AppError
fmt.Println(err) // code: 00A, msg: app error occurred
wrappedErr := errors.Unwrap(err)
fmt.Printf("%T\n", err) // *main.LibError
fmt.Println(wrappedErr) // error occured in Lib
fmt.Println(errors.Is(err, ErrParse)) // true
}
上記のようにUnwrap() error
のメソッドを実装すればアプリケーションのエラーハンドリングにてIs関数とAs関数をライブラリ提供のエラー値に対して適用することができます。