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
- 実装はここ https://github.com/golang/tools/blob/master/go/analysis/passes/errorsas/errorsas.go
- go test時にチェックしてくれーというissue https://github.com/golang/go/issues/31213
追記
場合によっては、pkg/errorsのAs()が使われることがある。このようなときに go vet
はチェックしてくれない。そんなわけで pkg/errors
と併存したコード上でCIを突き抜けての実行時panicが起きうるみたい。