【Go 1.20 の予習】複数 error を返す Unwrap メソッドについて

おそらく 2022-02 にリリースされる Go 1.20 で errors パッケージの仕様が変わるみたいなので予習しておく。

Wrapping multiple errors

Go 1.20 expands support for error wrapping to permit an error to wrap multiple other errors.

An error e can wrap more than one error by providing an Unwrap method that returns a []error.

The errors.Is and errors.As functions have been updated to inspect multiply wrapped errors.

The fmt.Errorf function now supports multiple occurrences of the %w format verb, which will cause it to return an error that wraps all of those error operands.

The new function errors.Join returns an error wrapping a list of errors.

現行の errors.Is() および errors.As() 各関数では 対象となる error インスタンスについて型アサーションを行い Unwrap() error メソッドを含む型か否かで再帰的に処理を行っているが, Go 1.20 からは,この評価に Unwrap() []error メソッドが加わる。

具体的にはこんな感じらしい。

// Is reports whether any error in err's tree matches target.
//
// The tree consists of err itself, followed by the errors obtained by repeatedly
// calling Unwrap. When err wraps multiple errors, Is examines err followed by a
// depth-first traversal of its children.
//
// An error is considered to match a target if it is equal to that target or if
// it implements a method Is(error) bool such that Is(target) returns true.
//
// An error type might provide an Is method so it can be treated as equivalent
// to an existing error. For example, if MyError defines
//
//    func (m MyError) Is(target error) bool { return target == fs.ErrExist }
//
// then Is(MyError{}, fs.ErrExist) returns true. See syscall.Errno.Is for
// an example in the standard library. An Is method should only shallowly
// compare err and the target and not call Unwrap on either.
func Is(err, target error) bool {
    if target == nil {
        return err == target
    }

    isComparable := reflectlite.TypeOf(target).Comparable()
    for {
        if isComparable && err == target {
            return true
        }
        if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
            return true
        }
        switch x := err.(type) {
        case interface{ Unwrap() error }:
            err = x.Unwrap()
            if err == nil {
                return false
            }
        case interface{ Unwrap() []error }:
            for _, err := range x.Unwrap() {
                if Is(err, target) {
                    return true
                }
            }
            return false
        default:
            return false
        }
    }
}

// As finds the first error in err's tree that matches target, and if one is found, sets
// target to that error value and returns true. Otherwise, it returns false.
//
// The tree consists of err itself, followed by the errors obtained by repeatedly
// calling Unwrap. When err wraps multiple errors, As examines err followed by a
// depth-first traversal of its children.
//
// An error matches target if the error's concrete value is assignable to the value
// pointed to by target, or if the error has a method As(interface{}) bool such that
// As(target) returns true. In the latter case, the As method is responsible for
// setting target.
//
// An error type might provide an As method so it can be treated as if it were a
// different error type.
//
// As panics if target is not a non-nil pointer to either a type that implements
// error, or to any interface type.
func As(err error, target any) bool {
    if err == nil {
        return false
    }
    if target == nil {
        panic("errors: target cannot be nil")
    }
    val := reflectlite.ValueOf(target)
    typ := val.Type()
    if typ.Kind() != reflectlite.Ptr || val.IsNil() {
        panic("errors: target must be a non-nil pointer")
    }
    targetType := typ.Elem()
    if targetType.Kind() != reflectlite.Interface && !targetType.Implements(errorType) {
        panic("errors: *target must be interface or implement error")
    }
    for {
        if reflectlite.TypeOf(err).AssignableTo(targetType) {
            val.Elem().Set(reflectlite.ValueOf(err))
            return true
        }
        if x, ok := err.(interface{ As(any) bool }); ok && x.As(target) {
            return true
        }
        switch x := err.(type) {
        case interface{ Unwrap() error }:
            err = x.Unwrap()
            if err == nil {
                return false
            }
        case interface{ Unwrap() []error }:
            for _, err := range x.Unwrap() {
                if As(err, target) {
                    return true
                }
            }
            return false
        default:
            return false
        }
    }
}

ちょっと長くて申し訳ないが,各関数の型 switch 文のなかで Unwrap() []error メソッドを含む型を評価しているのがおわかりだろうか。

標準パッケージでは新設の errors.Join() 関数や fmt.Errorf() 関数の拡張でマルチエラーに対応するようだが,自前で error 型を作る場合でも Unwrap() []error メソッドを追加することで errors.Is() 関数や errors.As() 関数による評価が可能になるわけだ。

ブックマーク

参考図書

photo
プログラミング言語Go
アラン・ドノバン (著), ブライアン・カーニハン (著), 柴田芳樹 (著)
丸善出版 2016-06-20 (Release 2021-07-13)
Kindle版
B099928SJD (ASIN)
評価     

Kindle 版出た! 一部内容が古びてしまったが,この本は Go 言語の教科書と言ってもいいだろう。感想はこちら

reviewed by Spiegel on 2021-05-22 (powered by PA-APIv5)

photo
初めてのGo言語 ―他言語プログラマーのためのイディオマティックGo実践ガイド
Jon Bodner (著), 武舎 広幸 (翻訳)
オライリージャパン 2022-09-26
単行本(ソフトカバー)
4814400047 (ASIN), 9784814400041 (EAN), 4814400047 (ISBN)
評価     

2021年に出た “Learning Go” の邦訳版。私は版元で PDF 版を購入。 Go 特有の語法(idiom)を切り口として Go の機能やパッケージを解説している。 Go 1.19 対応。

reviewed by Spiegel on 2022-10-11 (powered by PA-APIv5)

photo
実用 Go言語 ―システム開発の現場で知っておきたいアドバイス
渋川 よしき (著), 辻 大志郎 (著), 真野 隼記 (著)
オライリージャパン 2022-04-22
単行本(ソフトカバー)
4873119693 (ASIN), 9784873119694 (EAN), 4873119693 (ISBN)
評価     

版元のデジタル版を購入。 Go で躓きやすい点を解説していくのが最初の動機らしい。「◯◯するには」を調べる際にこの本を調べるといいかも。

reviewed by Spiegel on 2022-10-26 (powered by PA-APIv5)