goのerrors.As()でpanicしちゃう話

ドキュメントに書いてあることだけど、渡し方をミスるとerrors.As()はpanicしてしまう。

As panics if target is not a non-nil pointer to either a type that implements error, or to any interface type.

errors.As()の使い方

その前に errors.As()の使い方のおさらい。

errors.As() はエラー値の基底が第二引数で渡された形状の型だったときにtrueを返す関数1

例えばexampleの例のコードはこんな感じ。

package main

import (
    "errors"
    "fmt"
    "os"
)

func main() {
    if _, err := os.Open("non-existing"); err != nil {
        var pathError *os.PathError
        if errors.As(err, &pathError) {
            fmt.Println("Failed at path:", pathError.Path)
        } else {
            fmt.Println(err)
        }
    }
}

結果。os.PathErrorの方に分岐されている。

Failed at path: non-existing

使いみち

pkg/errors.Cause()errors.Is() ではエラー値の基底が対象の値かどうかの一致を見るチェックになってしまう。Asは型かどうか(interfaceかどうか)。ポインターで変数を初期化してそれに参照でbindするので結構ユニークな(あまり見慣れない)使い方かもしれない。

ng例

こういうエラーメッセージになる。注意深く見ていけば気付けるがたまに見落とす。見落としたときは実行時エラーになるので辛い。

panic: errors: *target must be interface or implement error

goroutine 1 [running]:
errors.As(0x10ec580, 0xc000104180, 0x10b6180, 0xc0001041b0, 0x0)
    /opt/local/lib/go/src/errors/wrap.go:87 +0x513
main.main()
    CWD/01errors-as-ng/main.go:12 +0xc5
exit status 2

この様なpanicになるのは以下の様なとき2

--- 00errors-as-ok/main.go   2020-09-09 14:35:45.000000000 +0900
+++ 01errors-as-ng/main.go    2020-09-09 14:19:36.000000000 +0900
@@ -8,7 +8,7 @@
 
 func main() {
    if _, err := os.Open("non-existing"); err != nil {
-      var pathError *os.PathError
+       var pathError os.PathError
        if errors.As(err, &pathError) {
            fmt.Println("Failed at path:", pathError.Path)
        } else {
--- 00errors-as-ok/main.go   2020-09-09 14:35:45.000000000 +0900
+++ 02errors-as-ng/main.go    2020-09-09 14:25:28.000000000 +0900
@@ -9,7 +9,7 @@
 func main() {
    if _, err := os.Open("non-existing"); err != nil {
        var pathError *os.PathError
-      if errors.As(err, &pathError) {
+       if errors.As(err, pathError) {
            fmt.Println("Failed at path:", pathError.Path)
        } else {
            fmt.Println(err)

実行時エラーなので厳しい。この種のpanicの何が嫌かといえば、滅多にないエラーハンドリングに対する初回のログの失われが起きうること。

interfaceを期待したい場合

丁寧に考えればわかることではあるけれど。interface自体は最初から参照を持つのでinterfaceとして受け取りたい場合には以下の様になる。

--- 00errors-as-ok/main.go   2020-09-09 14:35:45.000000000 +0900
+++ 04errors-as-ok/main.go    2020-09-09 14:35:28.000000000 +0900
@@ -6,11 +6,15 @@
    "os"
 )
 
+type hasTimeout interface {
+   Timeout() bool
+}
+
 func main() {
    if _, err := os.Open("non-existing"); err != nil {
-      var pathError *os.PathError
+       var pathError hasTimeout
        if errors.As(err, &pathError) {
-          fmt.Println("Failed at path:", pathError.Path)
+           fmt.Println("Failed at path:", pathError, "!!", pathError.Timeout())
        } else {
            fmt.Println(err)
        }

結果

Failed at path: open non-existing: no such file or directory !! false

もちろん参照を渡していない場合にはエラーになりますね(この逆はbuild時にコンパイルエラーになってくれる)。

--- 04errors-as-ok/main.go   2020-09-09 14:48:45.000000000 +0900
+++ 05errors-as-ng/main.go    2020-09-09 14:48:44.000000000 +0900
@@ -13,7 +13,7 @@
 func main() {
    if _, err := os.Open("non-existing"); err != nil {
        var pathError hasTimeout
-      if errors.As(err, &pathError) {
+       if errors.As(err, pathError) {
            fmt.Println("Failed at path:", pathError, "!!", pathError.Timeout())
        } else {
            fmt.Println(err)

こういうエラー。

panic: errors: target cannot be nil

goroutine 1 [running]:
errors.As(0x10ec5a0, 0xc000090180, 0x0, 0x0, 0x0)
    /opt/local/lib/go/src/errors/wrap.go:79 +0x5d4
main.main()
    CWD/05errors-as-ng/main.go:16 +0x98
exit status 2

おわり

エラーハンドリングで実行時エラーはかなりかなしいので、linterか何かが欲しいですね。

ちなみに環境は以下

$ go version
go version go1.15.1 darwin/amd64

追記

go vetでチェックしてくれそう。

$ go vet ./01errors-as-ng/
01errors-as-ng/main.go:12:6: second argument to errors.As must be a non-nil pointer to either a type that implements error, or to any interface type

追記

場合によっては、pkg/errorsのAs()が使われることがある。このようなときに go vet はチェックしてくれない。そんなわけで pkg/errors と併存したコード上でCIを突き抜けての実行時panicが起きうるみたい。


  1. 正確には基底とは限らないかも。Unwrap()の連鎖の中で見つかれば。

  2. エラーメッセージが答えのような気がするけれど。。