Help us understand the problem. What is going on with this article?

golang error handling (Go1.13)

初めに

自分で簡単なWebサービスを作っていくにあたり、どこでどういうエラーが起こったのかわかりにくく、エラー解消に多くの時間を割くということがおきた。そのため、エラーハンドリングを学び、この問題を解消したいと考えた。

version

go version go1.13.3

対象読者

  • エラーハンドリングって何?という人
  • エラーハンドリングをやっていきたいが、何が正解かわからない人
  • 一緒にエラーハンドリングについて考えてくれる方

躓いたところ

GolangのVersionが上がりライブラリerrorsに変更が加えられた。そのため、今までのエラーハンドリングと違うやり方になったらしい。それでは、どういう場合にどういうエラーハンドリングを採用すればいいかというところで沼にハマってしまった。
Go1.13 ライブラリerrors

エラーハンドリングをする意義

初めにで書いたように、エラーハンドリングは「どこに」「何をきっかけに」「どういう」エラーが起こったのかをサーバー側で判断できるようにする役割がある。また、もしクライアント側の操作にエラーがあった場合、どのようなエラーが起こりエラーが起こったのかを示すことで、クライアントに操作が間違っていることを示すことができる。

Goにおけるエラーとは

Goにおけるエラーは以下のエラーインターフェースを実装するに過ぎない。

type error interface {
    Error() string
}

そのため、自分でエラーを作るときは

type MyError struct {
    Err error   
    // other fields
}
func (e *MyError) Error() string { return ... }

とポインタレシーバを使って自分が作ったエラー用の構造体でError()を実装することで、エラーインターフェースを満たす。

エラーハンドリングの方法?

方法としては

  1. errorを単純に返す
  2. 型アサーションを使って場合分けをしてerror出力
  3. errorsを使ってerrorをWrapする

の3つではないかと。

errorを単純に返す

これは、今ままでに何回書いたかわからないが、

val, err := myFunction( args... );
if err != nil {
  return err
}
  // success

というerrorの基本的な返し方です。errの有無を err!=nilで確認することができ、errorがあった場合errorを返す。
この返し方の問題点としては、特定のエラーが発生したかどうかを確認することができないことにある。

型アサーションを使って場合分けをしてerror出力

ここで型アサーションについて

型アサーション は、インターフェースの値の基になる具体的な値を利用する手段を提供します。
インターフェースの値が特定の型を保持しているかどうかを テスト するために、型アサーションは2つの値(基になる値とアサーションが成功したかどうかを報告するブール値)を返すことができます。
t, ok := i.(T)
i が T を保持していれば、 t は基になる値になり、 ok は真(true)になります。

例えば、以下の例のように型アサーションを用いてエラーハンドリングを行う。(Go1.12以前)

package main

import (
    "errors"
    "fmt"
)

type MyError1 struct {
    status int
    err    error
}

func (me *MyError1) Error() string { return "get error1" }

type MyError2 struct {
    status int
    Err    error
}

func (me *MyError2) Error() string { return "get error2" }

func main(){
  if err := MyFunction(); err != nil {
        switch e := err.(type) {
        case *MyError1:
            fmt.Printf("%v", e)
        case *MyError2:
            fmt.Printf("%v", e)
        }
    }
}

上記のプログラムにおいてswitch文の際に使われているerr.(type)が型アサーションの部分である。
しかし、Go1.13で変わった。

package main

import (
    "errors"
    "fmt"
)

type MyError1 struct {
    status int
    err    error
}

func (me *MyError1) Error() string { return "get error1" }

type MyError2 struct {
    status int
    Err    error
}

func (me *MyError2) Error() string { return "get error2" }

func main() {
    var e1 *MyError1
    var e2 *MyError2
    if err := MyFunction(); err != nil {
        if errors.Is(err, e1) {
            fmt.Println(err)
        }
        if errors.Is(err, e2) {
            fmt.Println(err)
        }
    }
}

Go1.13で変わった点の一つは型アサーションを用いた条件分岐をerror.Is()関数を使って行うこととなった点である。
試したところ、以前のようにerr.(type)をSwith文を用いて実装することも場合によりますが可能なようです。
ここまででは、Is関数を使うメリットが見えてきませんが、Go1.13でともに追加されたWrapを使うときにIs関数が生きてきます。

errorsを使ってerrorをWrapする

今まで fmt.Errorf("%v",err) のように%vを使っていたのが大半でした。Go1.13からWrapをするための%wが登場しました。
Errorfは以下のように第1引数にフォーマットを決めるstring型、第2引数にinterface{}型で、返り値はerror型となっている。

fmt.Errorf("%w",err)error{}

書式指定子にエラーオペランドを持つ%wが含まれている場合、返されるエラーはUnwrapメソッドを実装している。(fmtパッケージ公式ドキュメント参照)
%wの登場によって、エラーをラップすることができ、ラップしてきたエラーを順にUnWrap()関数で出力することが可能となった。そのため、どこでエラーが発生したのか特定しやすくなったのではないか。

func WrapMyError() error {
    err := MySimpleError()
    return fmt.Errorf("%w", err)//型は*fmt.wrapError
}

ここで、エラーをラップしてしまうとどのようなエラーでも*fmt.wrapErrorになってしまう
そのため、

func main(){
  if err := WrapMyError(); err != nil {
        switch e := err.(type) {
        case *MyError1:
            log.Printf("case1:%v", e)
        case *MyError2:
            log.Printf("case2:%v", e)
        default:
            log.Print("default:%v",e)
        }
    }
}

このコードのSwitch分では常にdefaultに条件分岐してしまう。上記でも示したように fmt.Errorf("%w",err)がfmt.wrapError型を返すからです。ここでIs()関数が生きてきます。

func main(){
  if err := WrapMyError(); err != nil {
       //if err == MyError1{}と同じ
       if errors.Is(err,MyError1){...}
       //if err == MyError2{}と同じ
       if errors.Is(err,MyError2){...}
    }
}

このようにすることで、型がMyError1,MyError2なのか識別することができる。
また、if err != MyError1{} のように使いたい場合は今までと同様に!=を使えばいいそうだ。

次にUnWrap()関数について。

package main

import (
    "errors"
    "fmt"
)

var ErrNotFound = &SampleError{
    statusCode: 404,
    level:      "Error",
    msg:        "not found",
}

type SampleError struct {
    level      string
    statusCode int
    msg        string
}

func (e *SampleError) Error() string {
    return fmt.Sprintf("%s: code=%d, msg=%s", e.level, e.statusCode, e.msg)
}

func main() {
    err := func1()
    if err != nil {
        var sampleErr *SampleError
        if errors.As(err, &sampleErr) { //errが&sampleErr型ならerrの値がvar sampleErrに代入される
            switch sampleErr.level {
            case "Fatal":
                fmt.Printf("Fatal! %v\n", sampleErr)
            case "Error":
                fmt.Printf("Error! %v\n", sampleErr)
            case "Warning":
                fmt.Printf("Warning! %v\n", sampleErr)
            }
        }

        fmt.Printf("%+v\n", err)
        err = errors.Unwrap(err)
        fmt.Printf("%+v\n", err)
        err = errors.Unwrap(err)
        fmt.Printf("%+v\n", err)
        err = errors.Unwrap(err)
        fmt.Printf("%+v\n", err)
        return
    }

    fmt.Printf("エラーなし\n")
}

func func1() error {
    err := func2()
    if err != nil {
        return fmt.Errorf("func1 error: %w", err)
    }
    return nil
}

func func2() error {
    err := func3()
    if err != nil {
        return fmt.Errorf("func2 error: %w", err)
    }
    return nil
}
func func3() error {
    return ErrNotFound
}

参考文献[4]のサンプルコードを少しかえたものです「playground」。このサンプルコードが全てを語ってくれていると思う。実行結果は

Error! Error: code=404, msg=not found// <- func3のエラー
func1 error: func2 error: Error: code=404, msg=not found//<func1のエラー
func2 error: Error: code=404, msg=not found//<-func2のエラー
Error: code=404, msg=not found //<-func3のエラー
<nil>

のようになる。Unwrap()関数は

    14  func Unwrap(err error) error {
    15      u, ok := err.(interface {
    16          Unwrap() error
    17      })
    18      if !ok {
    19          return nil
    20      }
    21      return u.Unwrap()
    22  }

と定義されているため、err = errors.Unwrap(err)はラップされている次のエラーを変数errに格納している。そのため上記の実行結果では、func1->func2->func3というWrapされた順番(つまり、Errorf("%w",err)された順に)に出力されていることがわかる。

また、errors.As(err,&sampleErr)ではラップされているerrorの中に&sampleErr型が存在するかどうかを確認し、存在する場合&sampleErrをそのエラー値に設定している。そのため、sampleErrを出力すると、ラップされたエラー値が出力されている。確かに

As finds the first error in err's chain that matches target, and if so, sets target to that error value and returns true.
errのチェーン内でtargetに一致する最初のエラーを検出し、検出された場合、targetをそのエラー値に設定し、trueを返します。

Godocにあります。

以上のことをまとめると、

1.if err!=nil {}で条件分岐

var,err := MyFunction()
if err != nil{}

2.As関数

もしerrの形が*os.PathErrorならば、errの値が*osPathErrorに代入される。
okはbool型で、型が一致していたらtrue、していなかったらfalse

if e, ok := err.(*os.PathError); ok{}

to

var e *os.PathError
if errors.As(err, &e)

3.Is関数

if err == io.ErrUnexpectedEOF

to

if errors.Is(err, io.ErrUnexpectedEOF)

4.Wrap

fmt.Errorf("anything: %w", err)

%wを用いてラッピングをする。

5.Unwrap

err = errors.UnWrap(err)

この記述をすることで、WrapされたエラーをWrapされた順番でerrに代入。

My Best Answer

  • アーキテクチャをしっかり考慮した上でUnwrap,As,Isを使った実装がベストであると感じた。

ここまでで、私が気づいたこととしては、関数の返り値(err)をwrapして上位層の関数へerrorを渡すことができるということ。つまり、アーキテクチャがしっかりしているコードで初めて効果をはっきするのではないか。私が現在実装中のコード(レイヤードアーキテクチャ)ではInfra層(責務:技術的関心事)->UseCase層(責務:業務的関心事)->Handler層のように値を渡していくため、Infra層のerrorをWrapしUseCase層に渡し、UseCase層のエラーをHandler層に渡し、Handler層でUnWrapして用いる。このように、アーキテクチャを考えて実装しているコード上ではこのerrorのWrapは非常に生きてくると考えている。
 また、BadrequestなどのHandler層でのエラーなどはWrapする必要がないため、Switch文を使って処理をすることも可能なのではないかと感じた。
今回エラーを返すときに時間なども出力できたらと考え、fmt.Errorf()をエラー出力の際によく使っていたパッケージlogとerrors.New()を使って代用できないかと検討したが、断念した。代案として、自分で作ったエラー型にtime.Time型のフィールドを作って、出力させることでパッケージlogを使用しなくても詳細を出力できるのではないか。

  • errの有無は if err != nil{} で確認
  • 追加:statuscodeを用いたエラーハンドリングはhttp.Error(w,"・・・",statusCode)が便利

最後に

今回はエラーハンドリングについてまとめてみました。Go1.13にアップデートされたことから泥沼にハマってしまいましたが、なんとかまとめることができました。間違っている点があれば、教えていただけると幸いです。

参考文献

[1]githubWiki~GolangErrorValueFAQ~
[2]Go1.13のError wrappingを触ってみる
[3]The Go Blog-Working with Errors in Go 1.13
[4]Goの新しいerrors パッケージ xerrors
[5]Godoc-errors

yuukiyuuki327
学生です。アウトプットを行うために、Qiitaを利用しようと思っております。説明不足な点あるかと思いますが、よろしくお願いします。
techtrain
プロのエンジニアを目指すU30(30歳以下)の方に現役エンジニアにメンタリングもらえるコミュニティです。
https://techbowl.co.jp/techtrain/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした