接口类型变量
接口是 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
}
从输出结果可以看到:对于空接口类型变量,只有 _type
和 data
所指数据内容一致(不是数据指针的值一致)的情况下,两个空接口类型变量之间才能相等。
非空接口类型变量
与空接口类型变量一样,只有在 tab
和 data
所指的数据内容一致的情况下,两个非空接口类型变量之间才能相等。
这里就不再赘述了。
装箱原理
装箱(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)
上面的例子中,对 ei
和 i
两个接口类型变量的赋值均会触发装箱操作。
经过装箱后,箱内的数据(存放在新分配的内存空间中)与原变量便无瓜葛,除非是指针类型。
装箱操作由 Go 编译器和运行时共同完成。