Go Advent Calendar 2023の22日目の記事です。
はじめに
あるコードをリファクタリングしている際に得たnilの挙動についての記事となります。
コード
突然ですが、以下のコードがどのような挙動をすると思いますか?
(実際のコードを簡略化したものとなります)
func main() {
_, err := hoge()
if err != nil {
fmt.Println(err)
}
}
func hoge() (ret *struct{}, err error) {
defer func() {
if r := recover(); r != nil {
err = errors.New("hoge error")
}
}()
// なんか処理してエラーがあったらpanic
if true {
panic(ret)
}
return
}
正解は呼び出し元にてhoge error
が出力されます。
これはこれで、プロダクション環境において、正しく動いており、特に問題はなかったのですが、例えばpanic
にエラーではなく、構造体を入れていたりと、リファクタリングをする余地があったため、実施しようと思ったのですが、ふと疑問に思いました。
「retは構造体のポインタでnilになっているはずなのになぜrecoverの分岐に入るのだろうか」
Goにおけるnilの扱い
ドキュメントに解説がありました。
An interface value is nil only if the V and T are both unset, (T=nil, V is not set), In particular, a nil interface will always hold a nil type. If we store a nil pointer of type *int inside an interface value, the inner type will be *int regardless of the value of the pointer: (T=*int, V=nil). Such an interface value will therefore be non-nil even when the pointer value V inside is nil.
interface{}
においてのnil
とは型(T
)と値(V
)の両者がnil
で、nil
が成立するようです。
つまり、上記のr := recover()
の部分において、r
はinterface{}
をとるため、T
とV
がnil
にならなかったということになります。
実際に確認してみました。
func hoge() (ret *struct{}, err error) {
defer func() {
if r := recover(); r != nil {
fmt.Println(r == nil) //false
fmt.Println(r.(*struct{}) == nil) //true
fmt.Println("T:", reflect.TypeOf(r)) //T: *struct {}
fmt.Println("V:", reflect.ValueOf(r)) //V: <nil>
err = errors.New("hoge error")
}
}()
if true {
panic(ret)
}
return
}
結果の通り、V
はnil
となりましたが、T
は*struct{}
であるため、interface{}
としての比較r == nil
はfalse
となりました。一方で、*struct{}
にキャストした状態での比較r.(*struct{}) == nil
はtrue
となっています。
また上記の挙動についての日本語の記事はNoboNoboさんのこちらの記事がわかりやすかったです。
linterによる検知
今回はバグが発生することはありませんでしたが、時として、一見して分かりづらいこの挙動はバグの発生となる可能性もあります。しかし、すでにlinterによるgo-staticcheckがあるため、通常であれば、事前に防ぐことができます。
今回は少し特殊?なパターンだったため、検知されませんでしたが、、
例えば上記のドキュメントにあるようなerror
を例に取るとlinterによる検知が可能になりました。
func main() {
err := fuga(false)
if err != nil {
fmt.Println(err)
}
}
type MyErr struct{}
func (MyErr) Error() string {
return "MyErr"
}
func fuga(isErr bool) error {
var my *MyErr = nil
if isErr {
my = &MyErr{}
return my
}
return my
}
fuga
の戻り値であるerror
インターフェースは実際にMyErr
型であるため、err != nil
がtrue
になることはありませんが、VSCode以下の様に警告が出てきました。
終わりに
今回はnil
の挙動についての一部を紹介させていただきました。
普段はあまり意識することなく、コーディングできるかと思いますが、こういった挙動を知っているだけでも思わぬバグを踏まずに済むかもしれません。
では残り少ないアドベントカレンダーを楽しんでいきましょう!