作为 Go 开发者,你可能会认为自己对 nil 的行为已经了如指掌。但是,下面介绍的内容可能会让你大跌眼镜。
Code demo
让我们从一段简单的代码开始:
package main
import "reflect"
func main() {
var test any
println(test == nil)
println(test == (*string)(nil))
test = (*string)(nil)
println(test == nil)
println(test == (*string)(nil))
println(reflect.TypeOf(test).String())
}
乍一看,你可能以为这段代码会打印出一系列 true 语句。毕竟,我们要处理的是 nil 值,对吗?系好安全带,因为输出结果可能会超出你的认知:
true
false
false
true
*string
让我们来深入分析一下。
初始状态
当我们声明 var test any 时,我们创建了一个空接口类型的变量,它没有具体类型,也没有值。此时,test 确实是 nil,这就是为什么我们的第一次比较 test == nil 返回 true 的原因。
第一个转折
当我们比较 test == (*string)(nil) 时,得到的结果是 false。为什么呢?因为 (*string)(nil) 是一个类型化的 nil。它是指向字符串的 nil 指针,与未键入的 nil 不同。
进一步的分析
接下来,我们将 (*string)(nil) 赋值给 test。这一点至关重要,因为现在 test 拥有了一个具体类型(指向字符串的指针),尽管它的值仍然是 nil。这就是为什么 test == nil 现在返回 false 的原因!空接口 test 不再是 nil,因为它有了一个类型,尽管它的底层值是 nil。
浮出水面
比较测试 == (*string)(nil) 现在返回 true,因为双方的类型和值相同。
最后,我们使用 reflect.TypeOf(test).String() 来确认 test 确实是 *string 类型。
这种行为突出了 Go 的类型系统的一个重要方面:接口值只有在没有具体类型和值的情况下才是 nil。一旦我们将一个类型化的 nil 赋值给一个接口,它在比较中就不再被视为 nil,即使它的底层值是 nil。
如果不小心,这一行为可能会导致很难排查的错误,尤其是在处理错误或比较接口值时。
总结
Go 对 nil 和类型化 nil 值的处理是语言类型系统如何对看似简单的比较产生影响的例子。通过了解这些细微差别,开发者可以编写出更健壮、更可预测的 Go 代码。