Go 语言
接口
接口类型变量

接口类型变量

接口是 Go 这门静态类型语言中唯一“动静兼备”的语言特性。

  • 接口的静态特性:
  • 接口类型变量具有静态类型;
  • 支持在编译阶段的类型检查;
  • 接口的动态特性:
  • 接口类型变量兼具动态类型;
  • 接口类型变量在程序运行时可以被赋值为不同的动态变量。

内部表示

我们从 runtime 包中的找到接口类型变量在运行时的表示:

type iface struct {
    tab  *itab
    data unsafe.Pointer
}
 
type eface struct {
    _type *rtype
    data  unsafe.Pointer
}

在运行时层面,我们看到接口类型变量有两种内部表示:iface 和 eface。

  • eface:表示没有方法的空接口类型变量,即 interface 类型的变量;
  • iface:表示有方法的接口类型变量。

这两种结构的共同点是都有两个指针字段,并且第二个字段的功能相同,都指向当前赋值给该接口类型变量的动态类型变量的值。

不同的是,eface 所表示的空接口类型并无方法列表,因此其第一个指针字段指向一个 _type 类型结果,该结构为该接口类型变量的动态类型的信息:

type _type struct {
    size       uintptr
    ptrdata    uintptr
    hash       uint32
    tflag      tflag
    align      uint8
    fieldalign uint8
    kind       uint8
    alg        *typeAlg
    gcdata    *byte
    str       nameOff
    ptrToThis typeOff
}

而 iface 所表示的有方法的接口类型变量,其第一个指针字段指向一个 itab 结构,该结构包含了该接口类型变量的动态类型的方法列表:

type itab struct {
    inter *interfacetype
    _type *_type
    hash  uint32
    _     [4]byte
    fun   [1]uintptr
}

上面的 itab 结构的第一个字段 inter 指向的 interfacetype 结构存储着该接口类型自身的信息。interfacetype 的类型定义如下:

type interfacetype struct {
    typ     _type
    pkgpath name
    mhdr    []imethod
}

该结构的信息由类型信息,包路径名和接口方法集合的切片组成。

nil 接口变量

未赋值的接口类型变量的值为 nil, 这类变量即为 nil 接口变量。如下:

type error interface {
	Error() string
}
 
func printNilInterface() {
    // nil 接口变量
    var i interface{} // 空接口类型
    var err Error     // 非空接口类型
 
    println(i)          // (0x0, 0x0)
    println(err)        // (0x0, 0x0)
    println(i == nil)   // true
    println(err == nil) // true
    println(i == err)   // true
}

我们看到,无论是空接口类型变量还是非空接口类型变量,一旦变量值为 nil,那么它们内部表示均为 (0x0, 0x0),即类型信息和数据信息均为空。所以等值判断为 true

空接口类型变量

我们看一个空接口类型变量在内部表示的例子:

func printEmptyInterface() {
    var e1 interface{} // 空接口类型
    var e2 interface{} // 空接口类型
    var n, m int = 17, 18
 
    e1 = n
    e2 = m
 
    println(e1) // (0x45c6c0,0xc00003e728)
    println(e2) // (0x45c6c0,0xc00003e720)
    println(e1 == e2) // false
 
    e2 = 17
    println(e1) // (0x45c6c0,0xc00003e728)
    println(e2) // (0x45c6c0,0x47dc40)
    println(e1 == e2) // true
 
    e2 = int64(17)
    println(e1 == e2) // false
}

从输出结果可以看到:对于空接口类型变量,只有 _typedata 所指数据内容一致(不是数据指针的值一致)的情况下,两个空接口类型变量之间才能相等。

非空接口类型变量

与空接口类型变量一样,只有在 tabdata 所指的数据内容一致的情况下,两个非空接口类型变量之间才能相等。

这里就不再赘述了。

装箱原理

装箱(boxing)是编程语言领域的一个基础概念,一般是指把值类型转换成引用类型。 在 Go 语言中,将任意类型赋值给一个接口类型都是装箱操作。接口类型的装箱实则就是创建一个 eface 或 iface 的过程。

我们来看一个例子:

type T struct {
    n int
    s string
}
 
func (T) M1() {}
func (T) M2() {}
 
type NonEmptyInterface interface {
    M1()
    M2()
}
 
func main() {
    var t = T{
        n: 17,
        s: "hello",
    }
 
    var ei interface{}
    ei = t
    var i NonEmptyInterface
    i = t
 
    println(ei)
    println(i)
}
 
// 输出
// (0x461cc0,0xc00003e718)
// (0x47e0d8,0xc00003e700)

上面的例子中,对 eii 两个接口类型变量的赋值均会触发装箱操作。 经过装箱后,箱内的数据(存放在新分配的内存空间中)与原变量便无瓜葛,除非是指针类型。 装箱操作由 Go 编译器和运行时共同完成。