https://zenn.dev/msksgm/articles/20220325-unwrap-errors-is-as
Go ではデフォルトのエラーに対して、自作エラーを作成して wrap するように有識者の間では推奨されています。
ただ、wrap すると本来のエラーが隠れてしまいテストや実行時に単純な比較ができなくなります。
そこで、よく紹介されるのが、errors.Is
とerrors.As
です。erros.Is
とerrors.As
を使うと wrap 済みエラーに対して、元のエラーとの比較がおこなえます。
しかし、紹介記事では、そのままコピー&ペーストするとtrue
を返してほしいときに、false
を返してしまう方法を散見します。
本記事では、間違えてしまうパターンについて紹介し、解決方法を紹介します。 元のエラーが不要で、wrap したエラーのみで比較する実装のときには、本記事の手順は不要になります。
最初に、上手く比較できないパターンがどのようなときに発生するのか紹介します。
以下は、fmt.Errorf
を用いてエラーを wrap した実装です。errors.Is
とerrors.As
の結果は問題なくtrue
を返していることがわかります。
package main
import (
"errors"
"fmt"
"io/fs"
"os"
)
func main() {
if _, err := os.Open("non-existing"); err != nil {
var pathError *fs.PathError
wrapedErr := fmt.Errorf("err is %w", err)
if errors.As(wrapedErr, &pathError) {
fmt.Println("errors.As():Failed at path:", pathError.Path)
} else {
fmt.Println(err)
}
if errors.Is(wrapedErr, pathError) {
fmt.Println("errors.Is():Failed at path:", pathError.Path)
} else {
fmt.Println(err)
}
}
}
// Output
// errors.As():Failed at path: non-existing
// errors.Is():Failed at path: non-existing
続いて、自作エラーで wrap したパターンです。以下の実装ではうまくいきません。
一見、問題なく見えますが、errors.Is
とerrors.As
がfalse
を返していることがわかります。
なぜ、このようになるのでしょうか。
package main
import (
"errors"
"fmt"
"io/fs"
"os"
)
type SampleError struct {
message string
err error
}
func (se *SampleError) Error() string {
return se.message
}
func main() {
if _, err := os.Open("non-existing"); err != nil {
var pathError *fs.PathError
wrapedErr := &SampleError{message: "this is wraped err", err: err}
if errors.As(wrapedErr, &pathError) {
fmt.Println("errors.As():Failed at path:", pathError.Path)
} else {
fmt.Println(err)
}
if errors.Is(wrapedErr, pathError) {
fmt.Println("errors.Is():Failed at path:", pathError.Path)
} else {
fmt.Println(err)
}
}
}
// open non-existing: no such file or directory
// open non-existing: no such file or directory
前述の問題に対する、解決策を紹介します。
タイトル通り、自作エラーにUnwrap
を実装する必要があります。
package main
import (
"errors"
"fmt"
"io/fs"
"os"
)
type SampleError struct {
message string
err error
}
func (se *SampleError) Error() string {
return se.message
}
func (se *SampleError) Unwrap() error { // 追加
return se.err
}
func main() {
if _, err := os.Open("non-existing"); err != nil {
var pathError *fs.PathError
wrapedErr := &SampleError{message: "this is wraped err", err: err}
if errors.As(wrapedErr, &pathError) {
fmt.Println("errors.As():Failed at path:", pathError.Path)
} else {
fmt.Println(err)
}
if errors.Is(wrapedErr, pathError) {
fmt.Println("errors.Is():Failed at path:", pathError.Path)
} else {
fmt.Println(err)
}
}
}
// Output:
// errors.As():Failed at path: non-existing
// errors.Is():Failed at path: non-existing
問題に対する解決策は以上になります。
以降は、なぜUnwrap()
が必要なのか解説します。
この間違いの原因にはerrors.Is
とerrors.As
の特徴を確認する必要があります。
以下の表に違いをまとめました。
基本的には、エラーの比較方法は 2 あり、false を返す方法は共通であることがわかります。
色々書きましたが、Unwrap できなくなるまで実行されていることが重要です。errors.Is
と errors.As
の実装の詳細を確認します。
以下が、errors.Is
の実装です。
表にまとめた通りの実装になっていることがわかります。Unwrap
からnil
が帰ってきたら、false
を返して終了します。