小布最近又去面试了,遇到一个十分 tricky 的问题 - interface 与 nil 的比较。这篇文章就讨论下这个问题。这个问题在很多 golang 面试中经常被问到,我们需要了解接口类型在 Golang 中是如何构造的才能正确从底层回答这个问题。
Question: 下面哪一个 if 语句会返回 true?
package main
import "fmt"
type SomeType interface {
Get()
}
type SomeImpl struct {...}
func (i SomeImpl) Get() {...}
func main() {
var aType SomeType
if aType == nil {
fmt.Println("nil interface")
}
var aImpl *SomeImpl
if aImpl == nil {
fmt.Println("nil struct")
}
aType = aImpl
if aType == nil {
fmt.Println("nil assignment")
}
}
我们都知道,在 golang 中,空接口的值为 nil。空接口的默认值是 nil。这意味着未初始化变量 var aType SomeType 的值为 nil,因为它的类型是 empty(nil)接口。在这种情况下,如果条件满足,我们将在终端看到打印语句:
[Running] go run "main.go"
nil interface
这里还有一个未初始化的变量 var aImpl *SomeImpl
,它是指向结构体的指针。大家都知道,在 golang 中,所有内存都被初始化(清零),这意味着指针即使未被初始化,也会有一个默认值,即 nil。因此,这里又会输出一个 if 条件:
[Running] go run "main.go"
nil interface
nil struct
最后,我们看到一个结构指针值赋值(值初始化)到接口变量 aType = aImpl。根据前面的语句,我们可以合乎逻辑地假定,我们给变量 aType 赋值,结果 aType 将保持为 nil。
[Running] go run "main.go"
nil interface
nil struct
nil assignment
[Done] exited with code=0 in 0.318 seconds
这听起来是不是很合乎逻辑?好的,听起来不错(面试官会这么说)。让我们运行程序并检查结果。实际的输出结果是这样的:
[Running] go run "main.go"
nil interface
nil struct
[Done] exited with code=0 in 0.318 seconds
输出结果并不包含最后的 nil 赋值打印语句。要回答这个问题,我们必须深入语言实现,了解 golang 如何构建接口。
Interface
在 golang 中,接口是一种指定了一组方法签名的类型。当一个值被分配给一个接口时,golang 会构造一个接口值,它由两部分组成:动态类型和动态值。这通常被称为 "接口元组"。
Dynamic Type: 这是一个指向类型描述符的指针,描述了存储在接口中的具体值的类型。 Dynamic Value:这是一个指向接口持有的实际值的指针。
接口元组可以用以下结构来表示:
type iface struct {
tab *itab
data unsafe.Pointer
}
type itab struct {
inter *interfacetype
_type *_type
hash uint32
_ [4]byte
fun [1]uintptr // variable sized, actually [n]uintptr
}
tab:指向 itab 结构的指针,该结构包含该类型的信息以及该类型为接口实现的方法。 data: 指向接口实际数据的指针。
当一个值被赋值给接口时,golang 会找到被赋值给接口的具体类型的类型描述符。然后设置方法表 (itab),以便将通过接口调用的方法分派给正确的实现,最后将指向实际值的指针存储在接口的数据字段中。
所以当执行 aType = aImpl
时,
确定接口实现:golang 首先确定 *SomeImpl(指向 SomeImpl 的指针)实现了 SomeType 接口,因为 *SomeImpl 有一个签名正确的 Get() 方法。 查找类型描述符:golang 查找 *SomeImpl 的类型描述符。 创建 itab:golang 创建 itab 结构 指针赋值:golang 将指向 SomeImpl 值的指针赋值给接口的数据字段。示意图如下:
interface (aType)
+------------+ +-----------+
| tab |---------->| itab |
| | |-----------|
| data |--+ | inter |
+------------+ | | _type |
| | fun[0] |
| +-----------+
|
v
+---------------+
| *SomeImpl |
+---------------+
| ........ |
+---------------+
总结
综上所述, 在 golang 中,当检查接口是否为 nil 时,tab 字段和数据字段都必须为 nil。如果接口持有一个具体类型的 nil 指针,则 tab 字段不会为 nil,因此接口本身不会被视为 nil。
+ uninitialised +-------------+ initialised +
interface (aType) interface (aType)
+------------+ +--------------------------------+
| tab: nil | | tab: type of *SomeImpl |
| data: nil | | data: value of *SomeImpl (nil) |
+------------+ +--------------------------------+
这就是我们在终端中看不到最后一条打印语句的原因。执行 aType = aImpl
后,变量 aType 不再被视为 nil interface。