いつの間にかにpanic(nil)の挙動が変わってた
最近、Goでプログラムを書く機会が減っていたのだが、久々にGoを触って、個人用のアプリケーション開発に使うライブラリのバージョンを上げようとしたらテストで怒られてしまった。
何で怒られたんだろうと確認してみたら、どうやらGoのv1.21から panic(nil)
の挙動が変わっていることが原因の様子。後方互換性が基本的に保たれるGoで、バージョンを上げただけでテストが落ちたのが面白いので、備忘としてブログを書いてみる。
元々のライブラリの実装
元々(v1.20まで)は、panicの引数にnilを指定すると、recoverではnilが取得されていた。
package main
import "fmt"
func main() {
defer func() {
if e := recover(); e != nil {
fmt.Println("type: %[1]T, value: %[1]v\n", e)
return
}
fmt.Println("recover returns nil") // これが出力される
}()
panic(nil)
}
用意していたライブラリは、何かしらpanicが発生した場合にそれを error
に変換して返すもの。Goroutine実行時や、APIのMiddlewareなどで利用されることを想定していた。
通常はnilを引数にpanicさせることはないと思うけど、ライブラリということでそこもサポートできるように、下記のような(サンプルコード)を書いていた。
func sample() {
panicked := true
defer func() {
if r := recover(); r != nil || panicked {
err = recovery.Recovery(r)
}
}()
fmt.Println("your code")
panicked = false
return nil
}
panic発生有無を確認するための変数 panicked
を定義して判断する実装は、v1.4の頃のGo gRPC Middlewareを参考にしていた。
テストはとても単純で、 panic(nil)
をrecoverしたときのエラーメッセージを前方一致でassertしていた(該当箇所)。
v1.21からの挙動
v1.21から panic(nil)
したものをrecoverした場合、 *runtime.PanicNilError
を取得するようになった。
Go 1.21 now defines that if a goroutine is panicking and recover was called directly by a deferred function, the return value of recover is guaranteed not to be nil. To ensure this, calling panic with a nil interface value (or an untyped nil) causes a run-time panic of type *runtime.PanicNilError.
Go 1.21では、goroutineがパニックを起こし、recoverがdefer関数から直接呼び出された場合、recoverの戻り値がnilでないことが保証されると定義されました。これを保証するために、nilインタフェース値(または型付けされていないnil)でpanicを呼び出すと、*runtime.PanicNilError型のランタイムパニックが発生します。
その結果、本当にpanicが発生したのかを確認する余計なコードも書かなくて良くなった。すっきり。
バージョンを上げただけでテストが落ちたけど、panicの引数にnilを指定するような人は基本的にいないはずなので、実影響がある人はそうそういないでしょう。むしろ、Middlewareでrecoverするように実装していたのに、不慣れな人がアプリケーション内部に panic(nil)
を紛れ込ませてしまっても期待通りの挙動になるので、嬉しい人の方が多いはず。
Goはv1.22でも、ループの変数に関して互換性のない修正が入ったりしていますが、これもおそらく多くのGopherを悩ませていたGoのイケていない挙動のひとつだったので、この修正を心待ちにしていた人も多かったはず。バージョンアップ時には気をつけないといけませんが、言語が良い方向にアップデートされるのは嬉しいですね。
Discussion