フューチャー技術ブログ

Go 1.20 Wrapping multiple errors

はじめに

こんにちは!TIG の川口です。
本記事は Go 1.20リリース連載 の3本目です。Wrapping multiple errors についてお話します。

Release Note では、こちら の箇所になります。
Proposal は、こちら になります。

何が変わったか?

さて具体的に何が変わったかをはじめにおさえておきます。

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.

要約すると、以下のようになるでしょうか。 (以降、wrapping multiple errors はマルチエラーと記載します。)

  • エラーのラップが拡張されて、複数のエラーをラップしたマルチエラーを作成できるようになりました。
  • error 型は、 []error を返す Unwrap メソッドを提供することで、マルチエラーとすることができます。
  • マルチエラーに関して、 errors.Is 関数と errors.As 関数により、検査できるようになりました。
  • fmt.Errorf 関数と errors.Join 関数により、マルチエラーを作成できるようになりました。
  • (マルチエラーを []error に復元する方法については言及していません。)

目次

それでは以降、下記の順に則ってお話していこうと思います。

  1. マルチエラーの作成方法
  2. マルチエラーの検査方法
  3. マルチエラーを []error に復元する方法
  4. どんなときに使えるか?

また、本稿では以下の version を利用しています。

$ go version
go version go1.20rc3 darwin/amd64

マルチエラーの作成方法

マルチエラーの作成方法に関しては、先述の通り以下の2パターンあるようです。

package main

import (
"errors"
"fmt"
"log"
"reflect"
)

func main() {
err1 := errors.New("err1")
err2 := errors.New("err2")

// errors.Joins を使う方法
errByErrors := errors.Join(err1, err2)
log.Println(reflect.TypeOf(errByErrors)) // *errors.joinError

// fmt.Errorf を使う方法
errByFmt := fmt.Errorf("err: %w, %w", err1, err2)
log.Println(reflect.TypeOf(errByFmt)) // *fmt.wrapErrors
}

ただしそれぞれ作成される error の型は異なります。詳細は、以下を参照ください。
またこちらの実装を見ると、どちらも、[]error を返す Unwrap メソッドが提供されていることがわかるかと思います。

マルチエラーの検査方法

今までは、 errors.Is, errors.As を使ってラップされたエラーの検査を行うことができました。
今後はこれらの関数が、マルチエラーにも対応するようです。

package main

import (
"errors"
"fmt"
"log"
)

func main() {
err1 := errors.New("err1")
err2 := errors.New("err2")
err3 := errors.New("err3")

// errors.Joins を使う方法
errByErrors := errors.Join(err1, err2)
log.Println(
errors.Is(errByErrors, err1), // true
errors.Is(errByErrors, err2), // true
errors.Is(errByErrors, err3), // false
)

// fmt.Errorf を使う方法
errByFmt := fmt.Errorf("err: %w, %w", err1, err2)
log.Println(
errors.Is(errByFmt, err1), // true
errors.Is(errByFmt, err2), // true
errors.Is(errByFmt, err3), // false
)
}

基本的な考え方は、既存のものと変更はなさそうです。
既存のラップされたエラーの機構と互換性を保つように Unwrap() errorUnwrap() []error メソッドのどちらでも再帰的に処理できるようになっていそうですね。

マルチエラーを []error に復元する方法

こちらに関しては言及がなかったので、特別何か専用の関数が増えたりということは今のところなさそうですね。
こちらを実現しようとすると、👆の errors.Is, errors.As 等の実装を見ていただいた方はわかるかと思いますが、以下のようにしなければならなさそうです。

package main

import (
"errors"
"log"
)

func main() {
err1 := errors.New("err1")
err2 := errors.New("err2")

errByErrors := errors.Join(err1, err2)
if errs, ok := errByErrors.(interface{ Unwrap() []error }); ok {
for _, err := range errs.Unwrap() {
log.Println(err)
}
}
}

ちなみに、そのまま errors.Unwrap を利用した際には、以下のように nil が返ってきてしまいます。

package main

import (
"errors"
"log"
)

func main() {
err1 := errors.New("err1")
err2 := errors.New("err2")

errByErrors := errors.Join(err1, err2)
log.Println(errors.Unwrap(errByErrors)) // nil
}

こちらはややおかしな挙動ではありますが、errors.Unwrap の実装を見ると理解できます。
マルチエラーは、Unwrap() []error メソッドは持っていても、Unwrap() error メソッドを持っていないからですね。

利用する場面

利用する場面としては、パッと思いつくものとしては「ベストエフォート的な複数処理」などがあるでしょうか。

例えば、「ベストエフォート的に各ユーザーに対して一人ずつメールを送信していく。」などの場面では使えそうです。
コードとしては以下のようなイメージです。(一括で処理できるように模索するべきとか。並列に処理をした方が良いとか。色々あるかもですが、その辺のお話はいったんおいておきます!!!)

func run() {
userIDs := []string{
"userID1",
"userID2",
"userID3",
}

for i := range userIDs {
err := sendEmail(userIDs[i])
if err != nil {
// err があっても、log だけ出力する
log.Println(err)
}
}
}

func sendEmail(userID string) error {
// Email を送信する
return nil
}

このとき、run() の返り値に error を増やしたいとなったときになかなかどのようなコードを書くか悩ましくなることが想像できると思います。
今までであれば、以下のようなサードパーティ製のものを使ったり自作のエラーパッケージを作成したりして実現していた方もいると思います。

それが今後は以下のようにして書き直せるようになるのかなと。

func run() error {
userIDs := []string{
"userID1",
"userID2",
"userID3",
}

var errs []error
for i := range userIDs {
err := sendEmail(userIDs[i])
if err != nil {
errs = append(errs, err)
}
}

return errors.Join(errs...)
}

func sendEmail(userID string) error {
// Email を送信する処理
return nil
}

おわりに

新しく拡張された Wrapping multiple erros についての記事でした!
用途がたくさんあるというわけではない気もしますが、今後の error handling の手法の一つとしておさえておいたほうが良いものになるかもしれません?🤔

次は宮永さんの timeのアップデート です。