Posted at

Goでのエラー判別

More than 1 year has passed since last update.


1. 返してきた関数/メソッドで判別

多分これが一番多いパターンです。

    if err := foo(); err != nil {

// fooが失敗したときの処理
}

v, err := bar()
if err != nil {
// barが失敗したときの処理
}

Goのエラーハンドリングはそれぞれの関数がエラーを返してきたとき、呼び出し側の関数で処理するのが原則です。

もし伝搬させたい場合はそのエラーをそのまま戻り値にして返します。

この場合更にその呼び出し元では「呼び出した関数が返したエラー」として一律で扱うべきです。

func foo() error {

if err := func1(); err != nil {
return err
}

if err := func2(); err != nil {
return err
}
return nil
}

func bar() {
if err := foo(); err != nil {
// `foo`が失敗したときの処理
// 中で何が失敗したかはここでは扱わない
}
}

しかし、ときには中の処理のどの部分で失敗したかによって処理を変えたい場合があります。


2. パッケージ変数で判別

2番めのパターンはパッケージ変数で判別するやり方です。

各エラーごとにそれぞれパッケージ変数を定義します。

呼び出し元ではそれらのエラー変数と比較してエラーを判別しハンドリングします。

package foo

import "errors"

var (
Err1 = errors.New("error 1")
Err2 = errors.New("error 2")
)

func Foo() error {
// 処理1
if process1.isFailed() {
return Err1
}
// 処理2
if process2.isFailed() {
return Err2
}
return nil
}

package main

import "foo"

func main() {
if err := foo.Foo(); err != nil {
switch err {
case foo.Err1:
// 処理1で失敗したときのエラーハンドリング
case foo.Err2:
// 処理2で失敗したときのエラーハンドリング
default:
// その他
}
}
}

このやり方では、呼び出した関数のどこで失敗したか判別することが出来ますし、更に下の階層で発生したエラーも判別することが出来ます。

しかし、このやり方は固定されたエラーケースには対応できますが、エラーの分類が複雑になったり、エラー時の状態を参照しないとハンドリングできないようなケースには対応できません。


3. 独自エラーの定義

3番目のパターンは独自のエラー型を定義するやり方です。

Goの組み込みのエラー型(error)は以下のような型です。

type error interface {

Error() string
}

そしてGoではシグネイチャーさえ満たせばどのような型でもそのインターフェース型として扱うことが出来ます。

そのため独自のエラー型を定義するのはとても簡単です。

type MyError struct {

Code uint32
Message string
}

// Errorメソッドさえ定義されていれば良い
func (err *MyError) Error() string { return err.Message }

func somethingFail() error { return &MyError{1, "my error"} }

呼び出し側でエラー判別する場合はtype switchを使いましょう。

    if err := somethingFail(); err != nil {

switch err := err.(type) {
case *MyError:
// MyErrorの処理
// switch文の頭で再定義したerr変数は*MyError型なので、
// MyError型の各フィールドにアクセスできる。
log.Println(err.Code, err.Message)

case *AnotherErrorType:
// AnotherErrorTypeの処理

case FooInterface:
// FooInterfaceの処理
// type FooInterface interface { Foo() string }
// FooInterfaceというインターフェースを満たすエラーはすべてここでハンドリングできる
log.Println(err.Foo())

default:
// その他の処理
}
}

大体この3つのパターンを使い分ければ大抵のケースに対応できるかなと思います。


バッドプラクティス

ちなみにエラーメッセージでハンドリングするのはバッドプラクティスです。

あくまで、表示やログ用と割り切りましょう。

    // こういうのはバッドプラクティスです

if err := somethingFail(); err != nil {
switch err.Error() {
case "error 1":
// error 1 の場合の処理
}
}

    // これはひどすぎる

if err := somethingFail(); err != nil {
myErr := new(MyError)
json.Unmarshal([]byte(err.Error()), myErr)
log.Println(myErr.Code, myErr.Message)
}


おまけ

エラーにスタックトレースや追加のエラーメッセージを付与したいときもあります。

そのようなときはgithub.com/pkg/errorsを活用しましょう。

参考:

https://qiita.com/Hiraku/items/ecd6c99854845f95e4f1

https://deeeet.com/writing/2016/04/25/go-pkg-errors/