[小布去面试]Tricky 的 Golang 面试题 - interface = nil

文摘   2024-07-26 22:10   中国香港  

小布最近又去面试了,遇到一个十分 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 时,

  1. 确定接口实现:golang 首先确定 *SomeImpl(指向 SomeImpl 的指针)实现了 SomeType 接口,因为 *SomeImpl 有一个签名正确的 Get() 方法。
  2. 查找类型描述符:golang 查找 *SomeImpl 的类型描述符。
  3. 创建 itab:golang 创建 itab 结构
  4. 指针赋值: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。


Go Official Blog
Golang官方博客的资讯翻译及独家解读
 最新文章