👋

Go言語で独自エラーを実装するときの実例(ライブラリ編)

2022/11/21に公開

概要

Goでライブラリを作るとき、呼び出し元でどのようにエラーハンドリングするか想像して実装することが出来ていますか?
この記事では、minimalなサンプルを提示し、Goでの独自エラー実装について紹介します。

TODO: Asについては、僕がユースケースを理解できていないので書いていません。

前提

Goの文法は分かっているということを仮定します。

ライブラリ利用者側から見たGoのエラー処理について

ご存知の通り、Goには高級?なエラー処理機構がありません。
関数が正常に終了したかどうかは、if文によって判定します。

func someFunc() error {...}

err := someFunc()
if err != nil {
    // 何か処理
}

また、errorインターフェースの定義は以下のようになっていて、.Error()によって出力する文字列を作成できれば、どんなものでもerror型として扱うことが出来ます。

type error interface{
    Error() string
}

そのため、ライブラリ作成者には独自エラーの構造体に対して、Error() stringを実装することが要求されます。

type myError struct{
    // some member
}

func (err *myError) Error() string{
    return ""
}

Unwrapの利用

上記のような独自エラーには、エラーが起こった原因となるエラーが何か判別出来ないという欠点がありました。
(正確には、判別のための標準的なインターフェースがなかった。)

Go1.13以降、エラー原因の特定インターフェースとして、errors.Unwrap(err) errorという関数が使えます。
これによって、ライブラリ使用者はエラーを引き起こしたエラーを取得することが出来ます。

err := someFunc()
if err != nil {
    inner := errors.Unwrap(err)
    fmt.Printf("内部エラーはこれ: %v", inner)
}

Isの利用

エラー原因特定のユースケースとして、エラー種別によって処理を分けるというケースがあります。
このとき、エラーの原因の原因の原因...のようにエラーが連鎖的にwrapされている場合、あるエラーと、その原因となったエラーの種別が同じものかを判定する必要があります。

そこで、errors.Is(err, target error) boolという関数が用意されています。
これによって、errnilになるまでUnwrapし続け、targetと等しいかチェック、処理を分けることが出来ます。

err := someFunc()
if err != nil {
    if errors.Is(err, ErrNotFound) {
        // なにか処理
    }else if errors.Is(err, ErrPermissionDenied) {
        // なにか処理
    }
}

ライブラリの実装者がすべき独自エラーの実装について

Unwrapの実装

Unwrap機能を提供するために、独自エラーはエラーを返すきっかけとなった内部エラーを保持しておく必要があります。
また、error型として扱うために、Error() stringメソッドを実装する必要があります。

type myError struct {
    innner error
}

func (err *myError)Error() string {
    return "myError: " + err.innner.Error()
}

ただ、これだけでは正しくUnwrapしてくれません。
そこで、errors.Unwrapの実装を見てどうすればいいか確認してみましょう。

https://cs.opensource.google/go/go/+/refs/tags/go1.19.3:src/errors/wrap.go;l=14

// Unwrap returns the result of calling the Unwrap method on err, if err's
// type contains an Unwrap method returning error.
// Otherwise, Unwrap returns nil.
func Unwrap(err error) error {
    u, ok := err.(interface {
        Unwrap() error
    })
    if !ok {
        return nil
    }
    return u.Unwrap()
}

関数内の最初に、err.(interface {Unwrap() error})という式があります。
これはerrが、無名interfaceであるinterface {Unwrap() error}を実装している型に変換可能かどうか、実行時にチェックするロジックです。
(型アサーション)

すなわち、Unwrapに投げられるエラーには、Unwrap() errorが実装されていることが期待されます。
(注意!errors.Unwrapとはシグネチャが違います)

myErrorにも、Unwrap() errorを実装してみます。

func (err *myError)Unwrap() error {
    return err.innner
}

func main() {
    err := someMyFunc() // 適当な関数
    innerErr := errors.Unwrap(err)
    fmt.Printf("内部エラー発見可能: %v", innerErr)
}

Isの実装

次に、errors.IsmyErrorについて呼び出された状況を考えます。

err := someMyFunc() // *myErrorを返す関数
if errors.Is(err, fs.ErrNotExist) {
    // どこかでファイルが存在しないことによるエラーが発生した
}

前節でUnrwapを実装したため、再帰的にerrを辿っていって、どこかでfs.ErrNotExistに当たらないかをチェックすることが出来ます。

しかし、独自エラー自体に種別があり、その種別について判定したい場合にはどうすれば良いでしょう。
とりあえず、errors.Isの実装を見てみます。

https://cs.opensource.google/go/go/+/refs/tags/go1.19.3:src/errors/wrap.go;l=40

// Is reports whether any error in err's chain matches target.
//
// The chain consists of err itself followed by the sequence of errors obtained by
// repeatedly calling Unwrap.
//
// An error is considered to match a target if it is equal to that target or if
// it implements a method Is(error) bool such that Is(target) returns true.
//
// An error type might provide an Is method so it can be treated as equivalent
// to an existing error. For example, if MyError defines
//
//    func (m MyError) Is(target error) bool { return target == fs.ErrExist }
//
// then Is(MyError{}, fs.ErrExist) returns true. See syscall.Errno.Is for
// an example in the standard library. An Is method should only shallowly
// compare err and the target and not call Unwrap on either.
func Is(err, target error) bool {
    if target == nil {
        return err == target
    }

    isComparable := reflectlite.TypeOf(target).Comparable()
    for {
        if isComparable && err == target {
            return true
        }
        if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
            return true
        }
        // TODO: consider supporting target.Is(err). This would allow
        // user-definable predicates, but also may allow for coping with sloppy
        // APIs, thereby making it easier to get away with them.
        if err = Unwrap(err); err == nil {
            return false
        }
    }
}

Unwrapより少し複雑ですが、

  1. targetが比較可能でerrと等しければtrue
  2. errIs(error) boolを実装する型であれば、err.Is(target)を使って判定
  3. 上のステップがfalseなら、Unwrapしてループ→1へ

といった処理になっています。
実装から分かるように、独自エラーに種別がある場合Is(error) boolメソッドの実装が必要です。

まずは、独自エラーに種別を定義してみます。
Error() stringも改良してみましょう。

type myError struct {
    kind  myStatus
    inner error
}

type myStatus string

const (
    statusNotFound      = myStatus("NotFound")
    statusUnauthorized  = myStatus("Unauthorized")
    statusCatInterfered = myStatus("CatInterfered")
)

func (err *myError) Error() string {
    return string(err.kind) + ": " + err.inner.Error()
}

myStatus型を作るor作らない、stringではなくintにする等、状況によって実装は変わりますが、myError
種別を保持するメンバ(kind)を加えるというのは共通するはずです。

続いて、myErrorに対してIs(error) boolメソッドを実装します。
また、ライブラリ利用者がerrors.Isの引数targetに入れるためのエラー種別を公開する変数を定義する必要があります。
(例えば、先程例示したfs.ErrNotExistのようなものです)

var (
    ErrNotFound      = myError{statusNotFound, nil}
    ErrUnauthorized  = myError{statusUnauthorized, nil}
    ErrCatInterfered = myError{statusCatInterfered, nil}
)

func (err *myError) Is(target error) bool {
    // `kind`で比較する前に、型変換で`myError`として扱えるかをチェック
    t, ok := target.(*myError)
    return ok && err.kind == t.kind
}

このように、「myError型として扱える」、「kindメンバが等しい」という2条件が揃ったときに2つのエラーが正しいとすると良いと思います。
またライブラリの利用者は、errors.Is(err, ErrNotFound)のように利用するものと想定しています。

実装のまとめ

これまでの実装で、以下のようなコードが出来上がります。

{{< gist tbistr 53e3905667cc3c3afd949e4d57357b5a >}}

おまけfmt.Errorf()を使うべきか

fmt.Errorf()のフォーマット指定子に%wを使うことで、渡されたエラーをwrapしたエラーを作ってくれます。
ただし、実装を見るとわかりますが、

https://cs.opensource.google/go/go/+/refs/tags/go1.19.3:src/fmt/errors.go;l=17

type wrapError struct {
    msg string
    err error
}

func (e *wrapError) Error() string {
    return e.msg
}

func (e *wrapError) Unwrap() error {
    return e.err
}

のように、wrapErrorという構造体に表示文字列と内部エラーを保持しているだけです。
すなわち、一番外側のエラーの種別情報に関して完全に捨てることになります。
個人的には、ライブラリのコードではfmt.Errorfは使わずに独自エラーを定義してあげて、有り得るエラーをvarで提示した方が親切だと思います。

独自エラーを作らないという考え

薄いラッパーや、関数毎に返すエラーが決まりきっている場合、下のレイヤのエラーをそのまま返すという選択もありだと思います。

例えば、os.Openのような関数ではos.PathErrorを返すとしています。
しかし、実体はfs.PathErrorです

また、そんなfsパッケージの中でも一部のエラーはoserrorのものをそのまま返しています。

https://cs.opensource.google/go/go/+/refs/tags/go1.19.3:src/io/fs/fs.go;l=136

// Generic file system errors.
// Errors returned by file systems can be tested against these errors
// using errors.Is.
var (
    ErrInvalid    = errInvalid()    // "invalid argument"
    ErrPermission = errPermission() // "permission denied"
    ErrExist      = errExist()      // "file already exists"
    ErrNotExist   = errNotExist()   // "file does not exist"
    ErrClosed     = errClosed()     // "file already closed"
)

func errInvalid() error    { return oserror.ErrInvalid }
func errPermission() error { return oserror.ErrPermission }
func errExist() error      { return oserror.ErrExist }
func errNotExist() error   { return oserror.ErrNotExist }
func errClosed() error     { return oserror.ErrClosed }
GitHubで編集を提案

Discussion