はじめに
今回は go 言語の型付 nil の直感的でない挙動について説明します。 まずは、次のコードを実行してみてください。 playground
package main
import "fmt"
func main() {
var x *int
var y *int = nil
var z error
fmt.Printf("x -> %#10v; x is nil -> %v\n", x, x == nil)
fmt.Printf("y -> %#10v; y is nil -> %v\n", y, y == nil)
fmt.Printf("z -> %#10v; z is nil -> %v\n", z, z == nil)
fmt.Printf("x isNil -> %v\n", isNil("x", x))
fmt.Printf("y isNil -> %v\n", isNil("y", y))
fmt.Printf("z isNil -> %v\n", isNil("z", z))
}
func isNil(name string, x interface{}) bool {
fmt.Printf("%s -> %#10v; ", name, x)
return x == nil
}
上のコードを実行結果は以下のようになります。
実行結果より、関数の引数を interface{}として受け取った際にx == nil
が型情報を持った nil の場合はイコールが true にならないことが分かります。
x -> (*int)(nil); x is nil -> true
y -> (*int)(nil); y is nil -> true
z -> <nil>; z is nil -> true
x -> (*int)(nil); x isNil -> false
y -> (*int)(nil); y isNil -> false
z -> <nil>; z isNil -> true
コード上ではどちらも同じ (*int)(nil)
であるのに、方やx==nil -> true
となり、方やx==nil -> false
となっています。とても不思議に見えます。
今回はなぜこのような挙動になるのかを見ていきます。
メモリの中身を確認
さきほどのコードを変更し、変数のメモリの中身を直接見てみます。playground
package main
import (
"fmt"
"unsafe"
)
func main() {
var x *int
var y *int = nil
var z error
fmt.Printf("byte(%v); x is nil -> %v\n", *(*[4]byte)(unsafe.Pointer(&x)), x == nil)
fmt.Printf("byte(%v); y is nil -> %v\n", *(*[4]byte)(unsafe.Pointer(&y)), y == nil)
fmt.Printf("byte(%v); z is nil -> %v\n", *(*[4]byte)(unsafe.Pointer(&z)), z == nil)
fmt.Printf("x isNil -> %v\n", isNil(x))
fmt.Printf("y isNil -> %v\n", isNil(y))
fmt.Printf("z isNil -> %v\n", isNil(z))
}
func isNil(x interface{}) bool {
fmt.Printf("byte(%v); ", *(*[4]byte)(unsafe.Pointer(&x)))
return x == nil
}
出力結果は以下のようになります。出力結果より、interface 化された値が本当は nil じゃないことがわかります。
(byte([0 0 0 0])
が nil ということです。)
byte([0 0 0 0]); x is nil -> true
byte([0 0 0 0]); y is nil -> true
byte([0 0 0 0]); z is nil -> true
byte([128 22 74 0]); x isNil -> false
byte([128 22 74 0]); y isNil -> false
byte([0 0 0 0]); z isNil -> true
それでは、なぜ interface 化すると、nil でなくなってしまうのでしょうか?
interface の構造
interface の中身を図示すると以下のような図になります。
interface は図のように2つのデータを持っています。
- 型情報(+interface で定義されたメソッド)
- 実際のデータへのアドレス(値がポインタより小さい場合は、直接保持している)
そのため、interface が持っているのは実際の値だけでなく、型の情報も含めて持っているということになります。これが最初の挙動の正体です。
つまり、interface 化された際に型の情報が保存されたため、アドレスが何も指していない状態==nil
ではなくなってしまい、nil とのイコールが成り立たなくなってしまった、ということです。すごいトラップです。
まとめ
型の情報を持った nil を interface として関数の引数にしたため、型情報をがアドレス空間に保存されてしまい、nil でなくなってしまう、ということでした。
また、interface の nil(最初の例で言えば error 型)で宣言した場合は、そもそも型情報が欠落しているため前述の挙動にならず純粋な nil のままとなります。その場合は、直感的な挙動になるため、もしも自前の error 型を作成する際などは今回のことを念頭に置いておき、型付 nil を作成しないように注意することが大切です。