goのnilは直感的ではない、これは強烈にハマりそう。
型を持つnil
nilと一口に書くが、実際には型がある。
nilとnilが等価でないように見える
nilが型情報を持つので、nil == nilがtrueになるとは限らない。
trueとなるためには、右辺と左辺の「nil」の型が一致しているという条件が必要。
package main
func main() {
var x *int32 = nil
var y *int64 = nil
equals(x, y)
return
}
func equals(x, y interface{}) {
println(x == y)
}
> false
より凶悪なのが以下のようなパターンで、equals()内ではxがinterface{}として扱われているので、ここでnilを書くと左辺もinterface{}型のnilとして扱われる。
型が一致しないため、この結果もfalseとなる。
package main
func main() {
var x *int32 = nil
isnil(x)
return
}
func isnil(x interface{}) {
println(x == nil)
}
> false
nilはキャスト出来る
型があることを実感できる事実として、nilは型キャストすることが出来る。先のコードでfalseとなっていた例も、等価式のnilをキャストしてやればtrueを返せる。
package main
func main() {
var x *int32 = nil
isnil(x)
return
}
func isnil(x interface{}) {
println(x == (*int32)(nil))
}
> true
つまり、オブジェクトをinterface{}として扱っている場合、等価演算子のみを用いてもnilであることを確実に知ることはできない。
確実に「nil」を検出する
nilを検出するにはreflect.ValueのIsNil()を併用しなければならない。
ただし、reflect.ValueOf(nil)の場合はZeroValueが返却されるのでIsNil()が呼べない。なので、xがnilであるかの検査は先に行わなければならない。
# nilの型を意識する話なので補足しておくと、ここでreflect.ValueOf()はinterface{}型を引数として要求する。したがって、(interface{})(nil)の場合にZeroValueが返却される。
簡単に書けば以下のような感じ。
ちなみに、xがstructなどだとIsNil()がpanicになるので、実際には注意しなければいけない。
func isnil(x interface{}) {
println( (x == nil) || reflect.ValueOf(x).IsNil() )
}
型かと思う振る舞いをするnil
先のようにnilは型情報を持つが、nilが型そのもののように振舞っているように見えるケースがある。それが型スイッチ構文を使う場合で、以下のようにcaseに型と一緒にnilを含めることが出来る。
このとき、nilのケースにはinterface型のnil(つまりinterface{}(nil))の場合にのみ入る。
package main
func main() {
var x *int32 = nil
typeswitch(x)
return
}
func typeswitch(hoge interface{}) {
switch hoge.(type) {
case nil:
println("hoge is nil")
case *int32:
println("hoge is *int32")
case *int64:
println("hoge is *int64")
default:
println("unknown")
}
return
}
> hoge is *int32
「hoge is nil」と出すためには、main()の中で宣言しているxをinterface{}型にしておかなければならない。これはLanguage Specificationの「Type switches」にわかりやすい説明がある。
わかりづらい
goは比較的直感的に使える言語だと思うけれど、nilに関しては直感性が無く辛い。言語仕様を読めば、まぁソレっぽい事はちゃんと書いてあるんだけど・・・
また何か面白い挙動を見つけたら追記する。