Yappli Tech Blog

株式会社ヤプリの開発メンバーによるブログです。最新の技術情報からチーム・働き方に関するテーマまで、日々の熱い想いを持って発信していきます。

Goの独自エラーの作り方

サーバーサイドエンジニアの @shuymn です。

皆さん Go を書かれてらっしゃいますでしょうか。個人的に Go の強みの 1 つは組み込みや標準ライブラリで提供・利用されているインターフェースを利用した開発体験の良さがあげられると思っています。

そして Go では組み込みのインターフェースとして error インターフェースというものがあります。これはその名の通りエラーであることを表すインターフェースです。

type error interface {
    Error() string
}

単純明快なインターフェースですね。

この error インターフェースを満たす構造体をやりとりすることが Go のエラー処理の基本です。

そして、ライブラリから発生するエラーではなく自分のアプリケーション独自のエラーを定義したい場合は以下の 2 種類の方法から選択することになります。

  1. fmt.Errorferrors.New を使う*1
  2. error インターフェースを満たす構造体を自前で定義する

自分は 1 で用途を満たせるならそれを使い、満たせないなら 2 を使うというような判断をします。2 は構造体を自前で定義できるため 1 よりも柔軟に情報を持たせることができますが、その分取り扱いの難易度も上がります。

それぞれどのように定義、利用するか見ていきましょう。

fmt.Errorferrors.New を使う

これは標準パッケージである fmterrors の関数を使う方法です。それぞれ以下のように利用します。

var ErrFoo = fmt.Errorf("foo")
var ErrBar = errors.New("bar")

errors.New は文字列のみを受け付けますが、 fmt.Errorf は第二引数以降に変数を受け取ってそれを利用した文字列をエラーの文言にします。

そのため、上記の例のようにパッケージ変数として事前に固定のエラーを定義しておきたい場合は errors.New を使うことが多いです。そうではなく動的にエラーの文言を変えたい場合は fmt.Errorf を使います。

このように定義したエラーは err == ErrFoo のように比較で判定するのではなく、以下のように errors.Is を使って判定します。

func main() {
    err := doSomething()
    if err != nil {
        if errors.Is(err, ErrFoo) {
            fmt.Println("err is ErrFoo")
            return
        }
        fmt.Println("err is not ErrFoo")
        return
    }
}

func doSomething() error {
    return ErrFoo
}

Go Playground

このように、呼び出し元でエラーの種類を判別して処理を分岐させたい場合に利用します。Web API で、このエラーなら 400 系そうでないなら 500 系というように判断するために使ったりします。

上記の例では errors.Is(err, ErrFoo)err == ErrFoo にしても挙動は変化しませんが、doSomething 関数でエラーを Wrap*2すると正しく判定できなくなります*3

普通はエラーが発生した際に Wrap してどのような状況でエラーが発生したのか情報を付加していくことになるので、プロダクションコードでは err == ErrFoo は使ってはいけません。これを静的解析で検知したい場合はerr113という Linter が使えます*4

error インターフェースを満たす構造体を自前で定義する

先ほど紹介した独自エラーでは、エラーに付加できる情報というのは高々 1 つの文字列で表現できる範囲に限るため、複雑な情報を呼び出し元で取り出すことはできません。そこで、複雑な情報を付加できるようにしつつ error インターフェースを満たす方法として構造体を自前で定義するという方法もあります。

type MyError struct {
    When time.Time
}

func NewMyError() error {
    return &MyError{
        When: time.Now(),
    }
}

func (e *MyError) Error() string {
    return fmt.Sprintf("my error occurred at %s", e.When)
}

Go Playground

構造体を定義すれば必要なだけフィールドを追加して情報を持たせることができます。そして呼び出し側で error 型の変数を *MyError 型に変換する場合は errors.As を使います。

func main() {
    err := NewMyError()
    if err != nil {
        // ポインタレシーバでerrorインターフェースを実装しているので
        // 変数の型はMyErrorではなく*MyErrorにする
        var myerr *MyError
        if errors.As(err, &myerr) {
            fmt.Printf("err is MyError. when: %s", myerr.When.Format(time.RFC3339))
            return
        }
        fmt.Println("err is not MyError")
        return
    }
}

Go Playground

こちらも errors.Is のときと同様に通常の型変換 myerr, ok := err.(*MyError) はエラーが Wrap されている場合を考慮して、使ってはいけません。

また、コードコメントにも書きましたがインターフェースはポインタレシーバで実装してください。こちらについては説明が複雑になるため別の記事として近日公開予定です。公開したらこちらにも追記します。

(追記: 書きました)

tech.yappli.io

おわりに

エラーが発生したらとりあえず return err するのではなくそのエラーがどのようなエラーなのか立ち止まって考え、必要であれば独自エラーを定義して呼び出し元でそれを利用することが品質の高いサービスを提供する 1 つの鍵なのではないかと考えています。今回紹介した Go での独自エラーの作り方を参考にエラー処理の手札を増やしていただけたら非常に嬉しいです。

この記事は今年の 6 月に Yappli のサーバーチームの定例で自分が知見を共有するために作成したスライドをベースに執筆しています。Yappli では知見を共有するさまざまな場がありソフトウェアエンジニアにとって非常に刺激的な環境です。そんな Yappli に興味がありましたら是非カジュアル面談にご応募ください!

open.talentio.com

*1:標準ライブラリでは提供されていないスタックトレースなどが欲しい場合は pkg/errors などのサードパーティライブラリを利用します。

*2:エラーに対して情報を付加すること

*3:Go Playground

*4:golangci-lint でも goerr113 として使うことができます。

Copyright © 2021 Yappli, Inc. All rights reserved