接口的静态特性与动态特性
接口的静态特性体现在接口类型变量具有静态类型,比如var err error中变量 err 的静态类型为 error。拥有静态类型,那就意味着编译器会在编译阶段对所有接口类型变量的赋值操作进行类型检查,编译器会检查右值的类型是否实现了该接口方法集合中的所有方法。如果不满足,就会报错:
而接口的动态特性,就体现在接口类型变量在运行时还存储了右值的真实类型信息,这个右值的真实类型被称为接口类型变量的动态类型。你看一下下面示例代码:
var err error
err = errors.New("error1")
fmt.Printf("%T\n", err) // *errors.errorString
我们可以看到,这个示例通过 errros.New 构造了一个错误值,赋值给了 error 接口类型变量 err,并通过 fmt.Printf 函数输出接口类型变量 err 的动态类型为 *errors.errorString。
nil error 值 != nil
type MyError struct {
error
}
var ErrBad = MyError{
error: errors.New("bad things happened"),
}
func bad() bool {
return false
}
func returnsError() error {
var p *MyError = nil
if bad() {
p = &ErrBad
}
return p
}
func main() {
err := returnsError()
if err != nil {
fmt.Printf("error occur: %+v\n", err)
return
}
fmt.Println("ok")
}
真实的运行结果是什么样的呢?我们来看一下:
error occur: <nil>
接口类型变量的内部表示
// $GOROOT/src/runtime/runtime2.go
type iface struct {
tab *itab
data unsafe.Pointer
}
type eface struct {
_type *_type
data unsafe.Pointer
}
- eface 用于表示没有方法的空接口(empty interface)类型变量,也就是 interface{}类型的变量;
- iface 用于表示其余拥有方法的接口 interface 类型变量。
这两个结构的共同点是它们都有两个指针字段,并且第二个指针字段的功能相同,都是指向当前赋值给该接口类型变量的动态类型变量的值。
那它们的不同点在哪呢?就在于 eface 表示的空接口类型并没有方法列表,因此它的第一个指针字段指向一个_type类型结构,这个结构为该接口类型变量的动态类型的信息,它的定义是这样的:
// $GOROOT/src/runtime/type.go
type _type struct {
size uintptr
ptrdata uintptr // size of memory prefix holding all pointers
hash uint32
tflag tflag
align uint8
fieldAlign uint8
kind uint8
// function for comparing objects of this type
// (ptr to object A, ptr to object B) -> ==?
equal func(unsafe.Pointer, unsafe.Pointer) bool
// gcdata stores the GC type data for the garbage collector.
// If the KindGCProg bit is set in kind, gcdata is a GC program.
// Otherwise it is a ptrmask bitmap. See mbitmap.go for details.
gcdata *byte
str nameOff
ptrToThis typeOff
}
而 iface 除了要存储动态类型信息之外,还要存储接口本身的信息(接口的类型信息、方法列表信息等)以及动态类型所实现的方法的信息,因此 iface 的第一个字段指向一个itab类型结构。itab 结构的定义如下:
// $GOROOT/src/runtime/runtime2.go
type itab struct {
inter *interfacetype
_type *_type
hash uint32 // copy of _type.hash. Used for type switches.
_ [4]byte
fun [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
}
这里我们也可以看到,itab 结构中的第一个字段inter指向的 interfacetype 结构,存储着这个接口类型自身的信息。你看一下下面这段代码表示的 interfacetype 类型定义, 这个 interfacetype 结构由类型信息(typ)、包路径名(pkgpath)和接口方法集合切片(mhdr)组成。
// $GOROOT/src/runtime/type.go
type interfacetype struct {
typ _type
pkgpath name
mhdr []imethod
}
itab 结构中的字段_type则存储着这个接口类型变量的动态类型的信息,字段fun则是动态类型已实现的接口方法的调用地址数组。
下面我们再结合例子用图片来直观展现 eface 和 iface 的结构。首先我们看一个用 eface 表示的空接口类型变量的例子:
type T struct {
n int
s string
}
func main() {
var t = T {
n: 17,
s: "hello, interface",
}
var ei interface{} = t // Go运行时使用eface结构表示ei
}
这个例子中的空接口类型变量 ei 在 Go 运行时的表示是这样的:
我们看到空接口类型的表示较为简单,图中上半部分 _type 字段指向它的动态类型 T 的类型信息,下半部分的 data 则是指向一个 T 类型的实例值。
我们再来看一个更复杂的用 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: 18,
s: "hello, interface",
}
var i NonEmptyInterface = t
}
和 eface 比起来,iface 的表示稍微复杂些。我也画了一幅表示上面 NonEmptyInterface 接口类型变量在 Go 运行时表示的示意图:
由上面的这两幅图,我们可以看出,每个接口类型变量在运行时的表示都是由两部分组成的,针对不同接口类型我们可以简化记作:eface(_type, data)和iface(tab, data)。
而且,虽然 eface 和 iface 的第一个字段有所差别,但 tab 和 _type 可以统一看作是动态类型的类型信息。Go 语言中每种类型都会有唯一的 _type 信息,无论是内置原生类型,还是自定义类型都有。Go 运行时会为程序内的全部类型建立只读的共享 _type 信息表,因此拥有相同动态类型的同类接口类型变量的 _type/tab 信息是相同的。
而接口类型变量的 data 部分则是指向一个动态分配的内存空间,这个内存空间存储的是赋值给接口类型变量的动态类型变量的值。未显式初始化的接口类型变量的值为nil,也就是这个变量的 _type/tab 和 data 都为 nil。
而接口类型变量的 data 部分则是指向一个动态分配的内存空间,这个内存空间存储的是赋值给接口类型变量的动态类型变量的值。未显式初始化的接口类型变量的值为nil,也就是这个变量的 _type/tab 和 data 都为 nil。
当两个接口变量的 _type/tab 相同时,对 data 的相等判断要有区分。当接口变量的动态类型为指针类型时 (*T),Go 不会再额外分配内存存储指针值,而会将动态类型的指针值直接存入 data 字段中,这样 data 值的相等性决定了两个接口类型变量是否相等;当接口变量的动态类型为非指针类型 (T) 时,我们判断的将不是 data 指针的值是否相等,而是判断 data 指针指向的内存空间所存储的数据值是否相等,若相等,则两个接口类型变量相等。
在编译阶段,编译器会根据要输出的参数的类型将 println 替换为特定的函数,这些函数都定义在$GOROOT/src/runtime/print.go文件中,而针对 eface 和 iface 类型的打印函数实现如下:
// $GOROOT/src/runtime/print.go
func printeface(e eface) {
print("(", e._type, ",", e.data, ")")
}
func printiface(i iface) {
print("(", i.tab, ",", i.data, ")")
}
我们看到,printeface 和 printiface 会输出各自的两个指针字段的值。下面我们就来使用 println 函数输出各类接口类型变量的内部表示信息,并结合输出结果,解析接口类型变量的等值比较操作。
第一种:nil 接口变量
func printNilInterface() {
// nil接口变量
var i interface{} // 空接口类型
var err error // 非空接口类型
println(i)
println(err)
println("i = nil:", i == nil)
println("err = nil:", err == nil)
println("i = err:", i == err)
}
运行这个函数,输出结果是这样的:
(0x0,0x0)
(0x0,0x0)
i = nil: true
err = nil: true
i = err: true
第二种:空接口类型变量
func printEmptyInterface() {
var eif1 interface{} // 空接口类型
var eif2 interface{} // 空接口类型
var n, m int = 17, 18
eif1 = n
eif2 = m
println("eif1:", eif1)
println("eif2:", eif2)
println("eif1 = eif2:", eif1 == eif2) // false
eif2 = 17
println("eif1:", eif1)
println("eif2:", eif2)
println("eif1 = eif2:", eif1 == eif2) // true
eif2 = int64(17)
println("eif1:", eif1)
println("eif2:", eif2)
println("eif1 = eif2:", eif1 == eif2) // false
}
这个例子的运行输出结果是这样的:
eif1: (0x10ac580,0xc00007ef48)
eif2: (0x10ac580,0xc00007ef40)
eif1 = eif2: false
eif1: (0x10ac580,0xc00007ef48)
eif2: (0x10ac580,0x10eb3d0)
eif1 = eif2: true
eif1: (0x10ac580,0xc00007ef48)
eif2: (0x10ac640,0x10eb3d8)
eif1 = eif2: false
从输出结果中我们可以总结一下:对于空接口类型变量,只有 _type 和 data 所指数据内容一致的情况下,两个空接口类型变量之间才能划等号。另外,Go 在创建 eface 时一般会为 data 重新分配新内存空间,将动态类型变量的值复制到这块内存空间,并将 data 指针指向这块内存空间。因此我们多数情况下看到的 data 指针值都是不同的。
第三种:非空接口类型变量
type T int
func (t T) Error() string {
return "bad error"
}
func printNonEmptyInterface() {
var err1 error // 非空接口类型
var err2 error // 非空接口类型
err1 = (*T)(nil)
println("err1:", err1)
println("err1 = nil:", err1 == nil)
err1 = T(5)
err2 = T(6)
println("err1:", err1)
println("err2:", err2)
println("err1 = err2:", err1 == err2)
err2 = fmt.Errorf("%d\n", 5)
println("err1:", err1)
println("err2:", err2)
println("err1 = err2:", err1 == err2)
}
这个例子的运行输出结果如下:
err1: (0x10ed120,0x0)
err1 = nil: false
err1: (0x10ed1a0,0x10eb310)
err2: (0x10ed1a0,0x10eb318)
err1 = err2: false
err1: (0x10ed1a0,0x10eb310)
err2: (0x10ed0c0,0xc000010050)
err1 = err2: false
第四种:空接口类型变量与非空接口类型变量的等值比较
func printEmptyInterfaceAndNonEmptyInterface() {
var eif interface{} = T(5)
var err error = T(5)
println("eif:", eif)
println("err:", err)
println("eif = err:", eif == err)
err = T(6)
println("eif:", eif)
println("err:", err)
println("eif = err:", eif == err)
}
这个示例的输出结果如下:
eif: (0x10b3b00,0x10eb4d0)
err: (0x10ed380,0x10eb4d8)
eif = err: true
eif: (0x10b3b00,0x10eb4d0)
err: (0x10ed380,0x10eb4e0)
eif = err: false
此文章为3月Day20学习笔记,内容来源于极客时间《Tony Bai · Go 语言第一课》。