Go
golang

golangにおける独自エラー定義方法とエラーごとの処理分岐

More than 1 year has passed since last update.

内容

  • 独自エラーを生成する方法
  • 受け取り側で処理分岐できるエラーを作るには?

golangにおけるエラー

  • 下記インターフェースを実装した構造体
  • Error() は、エラーメッセージとしての文字列を出力する
type error interface {
    Error() string
}

独自エラーを生成する方法

errors.New(text string) error

  • エラーメッセージを文字列で指定
  • 実体の構造体は、errors パッケージで定義されている errorString

fmt.Errorf(format string, a ...interface{}) error

  • fmt.Sprintf と同様、フォーマットでエラーメッセージを指定
  • 内部的には errors.New(fmt.Sprintf(format, a)) を呼び出しているだけ
    • errors.New と同じく errorString

error インターフェースを実装した独自構造体を定義する

  • 定義した独自構造体のインスタンスを生成してエラーとして使用する
  • ①②との違いは、以下
    • 型を変えられる : 型アサーションによりエラー処理を分岐できる
    • メッセージ以外のプロパティを持たせられる : エラーにステータスを持たせられる

(参考) pkg/errors パッケージ

  • errors.Newerrors.Errorf メソッドが用意されており、①②を同一パッケージで書ける
  • 内部的な処理は微妙に違うが、統一感がでるので個人的にはこちらが好き

受け取り側で処理分岐できるエラーを作るには?

Error() で出力される文字列を比較する 【アンチパターン】

  • error としてはエラーメッセージ(文字列)を返すメソッドがあるだけ
  • エラーメッセージを比較すれば、所望のエラーかどうか確認することができる
package main

import (
    "errors"
    "log"
)

var (
    ErrMsg1 = "my error 1"
    ErrMsg2 = "my error 2"
)

func main() {
    if err := doError(); err != nil {
        switch err.Error() {
        case ErrMsg1:
            log.Fatalf(ErrMsg1)
        case ErrMsg2:
            log.Fatalf(ErrMsg2)
        }
    }
}

func doError(i int) error {
    switch i {
    case 1:
        return errors.New(ErrMsg1)
    case 2:
        return errors.New(ErrMsg2)
    }
    return nil
}

② エラーインスタンスを比較

  • パッケージのトップレベルにエラーインスタンスを作っておく
    • エラー生成側:事前生成したエラーインスタンスを返却する
    • エラー処理側:受け取ったエラーと事前生成したエラーインスタンスを比較する
package main

import (
    "errors"
    "log"
)

var (
    ErrMyError1 = errors.New("my error 1")
    ErrMyError2 = errors.New("my error 2")
)

func main() {
    if err := doError(); err != nil {
        switch err {
        case ErrMyError1:
            log.Fatalf("%#v", ErrMyError1)
        case ErrMyError2:
            log.Fatalf("%#v", ErrMyError2)
        }
    }
}

func doError(i int) error {
    switch i {
    case 1:
        return ErrMyError1
    case 2:
        return ErrMyError2
    }
    return nil
}

③ 独自定義したエラー構造体の型を比較

  • error インターフェースを実装した独自構造体を定義する
    • エラー生成側:定義した構造体を生成して返却する
    • エラー処理側:受け取ったエラーインスタンスに型アサーションを実施する
package main

import (
    "errors"
    "log"
)

type MyError1 struct {}

func (e *MyError1) Error() string {
    return "my error 1"
}

type MyError2 struct {}

func (e *MyError2) Error() string {
    return "my error 2"
}

func main() {
    if err := doError(); err != nil {
        switch e := err.(type) {
        case *MyError1:
            log.Fatalf("%#v", e)
        case *MyError2:
            log.Fatalf("%#v", e)
        }
    }
}

func doError(i int) error {
    switch i {
    case 1:
        return &MyError1{}
    case 2:
        return &MyError2{}
    }
    return nil
}

②と③の使い分け

  • 処理の分岐だけできれば良い場合

    • ②を使い、事前定義したインスタンスと比較する
  • 処理を分岐した上で、更に状態を取りたい場合

    • ③を使い、アサーションして構造体ごとの処理を呼ぶ
package main

import (
    "errors"
    "log"
)

type MyError1 struct {
    Status int
}

func (e *MyError1) Error() string {
    return "my error 1"
}

func main() {
    if err := doError(); err != nil {
        switch e := err.(type) {
        case *MyError1:
            // さらにこういった分岐をしたい場合は型アサーションにする 
            switch e.Status {
            case 404:
                log.Fatalf("%#v, %#v", e, 404)
            case 409:
                log.Fatalf("%#v, %#v", e, 409)
            }
        }
    }
}

func doError(i int) error {
    return &MyError1{Status: 404}
}

④ 番外編 : Assert errors for behaviour, not type

Inspecting errors | Dave Cheney より

Assert errors for behaviour, not type

  • エラーをチェックするメソッド IsXXX(err error) bool を定義する方法
  • 上記②③の方法の場合、呼び出し元が特定の変数/型に依存することになる
  • この方法であればインターフェースへの依存で済む
package main

import (
    "errors"
    "log"
)

type temporary interface {
    Temporary() bool
}

func (e *MyError1) Error() string {
    return "my error 1"
}

func (e *MyError1) Temporary() bool {
    return true
}

type MyError2 struct {}

func (e *MyError2) Error() string {
    return "my error 2"
}

func IsTemporary(err error) bool {
   te, ok := err.(temporary)
   return ok && te.Temporary()
}

func main() {
    if err := doError(); err != nil {
        if IsTemporary(err) {
            log.Fatalf("%#v", err)
        }
    }
}

func doError(i int) error {
    switch i {
    case 1:
        return &MyError1
    case 2:
        return &MyError2
    }
    return nil
}

(おまけ) errorを作れるパッケージたち

errors

  • New(message string) error

fmt

  • Errorf(format string, args ...interface{}) error

pkg/errors

エラーの新規作成

  • New(message string) error
    • errors.New とほぼ同じ
  • Errorf(format string, args ...interface{}) error
    • fmt.Errorf をほぼ同じ

エラーのラッピング

  • WithMessage(err error, message string) error
    • 元の error をラップし、メッセージを付与
  • WithStack(err error) error
    • 元の error をラップし、スタックトレースを付与
  • Wrap(err error, message string) error
  • Wrapf(err error, format string, args ...interface{}) error
    • 元の error をラップし、メッセージとスタックトレースを付与
  • Cause(err error) error
    • ラップされた error を取り出す

(おまけ) panicを起こせるパッケージたち

ビルトイン関数

  • panic(message string)

log

panic系

  • Panic(v ...interface{})
    • Panic is equivalent to Print() followed by a call to panic().
  • Panicln(v ...interface{})
    • Panicln is equivalent to Println() followed by a call to panic().
  • Panicf(format string, v ...interface{})
    • Panicf is equivalent to Printf() followed by a call to panic().

(参考)fatal系

  • Fatal(v ...interface{})
    • Fatal is equivalent to Print() followed by a call to os.Exit(1).
  • Fatalln(v ...interface{})
    • Fatalln is equivalent to Println() followed by a call to os.Exit(1).
  • Fatalf(format string, v ...interface{})
    • Fatalf is equivalent to Printf() followed by a call to os.Exit(1).

参考