LoginSignup
11
3

More than 3 years have passed since last update.

go1.13のerrorのラップ

Posted at

Go1.13からエラーがラップできるようになった

Go1.13のバージョンからエラーのラップができるようになったみたいなので、自分なりにまとめてみました。

新しく追加された関数

  • func As(err error, target interface{}) bool
  • func Is(err, target error) bool
  • func New(text string) error
  • func Unwrap(err error) error

エラーのラップ方法

まず、エラーのラップ方法はこんな感じになります

wrapError.go
package main

import(
    "errors"
    "fmt"
)

var MyError = myError()

func myError() error { return errors.New("myError")}

func makeMyError() error {
    return MyError
}

func makeWrappedError() error {
    err := makeMyError()
    return fmt.Errorf("%w", err)
}

func main() {
    err := makeMyError()
    fmt.Printf("%T\n", err)

    wrappedErr := makeWrappedError()
    fmt.Printf("%T\n", wrappedErr)
}

このようにエラーをwrapするときには fmt.Errorf("%w", err) を使います。
実行結果は以下

*errors.errorString
*fmt.wrapError

きちんとwrapされているのがわかります。
今度はこのエラーを取り出すのにUnwrapします。

unwrap.go

func main() {
    wrappedErr := makeWrappedError()
    fmt.Printf("type:%T", wrappedErr)

    unWrappedErr := errors.Unwrap(wrappedErr)
    fmt.Printf("type:%T", unWrappedErr)

    nilErr := errors.Unwrap(unWrappedErr)
    fmt.Printf("type:%T", nilErr)
}

結果は以下。

type:*fmt.wrapError
type:*errors.errorString
type:<nil>

このようにUnwrapされて行き、wrapしていないエラーをUnwrapすると、nilを返すようになります。
ここまでがエラーのラップについてです。

Is関数の使い方

エラーのラップはわかっていただけたと思いますが、これをどうやって使うの?という話になります。
まず、今までのエラーハンドリングについておさらいします。こんな感じ

errorHandle.go
func main() {
    err := makeMyError()
    if err != nil {
        switch err {
        case MyError:
            fmt.Printf("MyError: %s", err.Error())
        default:
            fmt.Printf("defaultError: %s", err.Error())
        }
    }
}

結果は以下

MyError: myError

これで、MyErrorを拾えています。ここでエラーをラップしたものに変えてみます。

wrapErrorHandle.go
func main() {
    err := makeWrappedMyError()
    if err != nil {
        switch err {
        case MyError:
            fmt.Printf("MyError: %s", err.Error())
        default:
            fmt.Printf("defaultError: %s", err.Error())
        }
    }
}

結果は

defaultError: myError

となってしまし、MyErrorとして拾えていません。ここで、Is関数の登場です。
Is関数を使うとこのようになります。

Is.go
func main() {
    err := makeWrappedError()
    if err != nil {
        if errors.Is(err, MyError) {
            fmt.Printf("MyError: %s", err.Error())
        } else {
            fmt.Printf("defaultError: %s", err.Error())
        }
    }
}
結果
MyError: myError

今度は、きちんとMyErrorとして拾えました。
このように、今までのエラーハンドリングでは扱えないパターンが出てくるので、Is関数を用いる必要が出てきます。

As関数の使い方

As関数もIs関数と使う目的は同じですが、引数を見ればわかるように自分で定義したエラーとの比較などを行うときに用います。
このような独自のエラーがあるとしましょう。

myError.go
package main

import(
    "fmt"
)

type marshalError struct {
    msg string
    code int
}

func (e *marshalError)Error() string {
    return fmt.Sprintf("marsshal error :%s, code: %d", e.msg, e.code)
}

func marshal() error {
    return &marshalError{msg:"hoge", code:0}
}

func main() {
    err := marshal()
    if e, ok := err.(*marshalError); ok {
        fmt.Printf("%s", e.Error())
    } else {
        fmt.Printf("defaultError")
    }
}

結果
marsshal error :hoge, code: 0

これで、構造体をマーシャル(仮想で)して帰ってきたエラーがエラーがmainErrorであることを確かめられました。
しかし、このエラーがまたラップされていた場合拾えなくなってしまいます。

wrappedMyError.go
package main

import(
    "fmt"
)

type marshalError struct {
    msg string
    code int
}

func (e *marshalError)Error() string {
    return fmt.Sprintf("marsshal error :%s, code: %d", e.msg, e.code)
}

func marshal() error {
    return &marshalError{msg:"hoge", code:0}
}

func wrapError() error {
    err := marshal()
    return fmt.Errorf("%w", err)
}

func main() {
    err := wrapError()
    if e, ok := err.(*marshalError); ok {
        fmt.Printf("%s", e.Error())
    } else {
        fmt.Printf("defaultError")
    }
}
結果
defaultError

これを避けるためにAs関数を使います。

As.go
package main

import(
    "errors"
    "fmt"
)

type marshalError struct {
    msg string
    code int
}

func (e *marshalError)Error() string {
    return fmt.Sprintf("marsshal error :%s, code: %d", e.msg, e.code)
}

func marshal() error {
    return &marshalError{msg:"hoge", code:0}
}

func wrapError() error {
    err := marshal()
    return fmt.Errorf("%w", err)
}

func main() {
    err := wrapError()
    var targetErr *marshalError
    if errors.As(err, &targetErr) {
        fmt.Printf("%s", targetErr)
    } else {
        fmt.Printf("defaultError")
    }
}


結果
mainError: main error

これで、mainErrorを拾えました。

最後にこれを応用して、エラーの抽象度をあげて、上位の関数に返すようなコードを考えてみました。

Example.go
package main

import(
    "errors"
    "fmt"
)

type User struct {
    name string
    email string
}
type invalidStructError struct {
    original interface{} //元の構造体
    err error
    code int
}

func (e *invalidStructError)Error() string {
    return fmt.Sprintf("original: %v, %s", e.original, e.err.Error())
}

func marshal(v interface{}) error {
    err := &invalidStructError{original:v, err: errors.New("invalid Struct"), code: 0}
    if err != nil{
        return fmt.Errorf("marshal err: %w", err)
    }
    return nil
}

func main() {
    val := User{name: "hoge", email: "1"}
    err := marshal(val)
    var target *invalidStructError
    if errors.As(err, &target) {
        fmt.Printf("code: %d\n", target.code)
        fmt.Printf("%s\n", target.Error())
    } else {
        fmt.Printf("defaultError")
    }
}
結果
code: 0
original: {hoge 1}, invalid Struct

As関数を用いてtargetにerrをUnwrapしているので、codeのフィールドが扱えるようになりました。

まとめ

このように、Go1.13ではエラーのラップができるようになりました。これにより、エラーをより上位の関数に渡すときに抽象化して渡すことができるようになります。
それに伴って、エラーのハンドリングが変わってくるので、新しい形式をうまく使いこなせると良さそうです。
まだまだgo初心者なので、間違いなどありましたらご指摘いただけると幸いです。

参考サイト

https://cipepser.hatenablog.com/entry/go1.13-error-wrapping
https://golang.org/pkg/errors/#pkg-index

11
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
3