一、Golang 接口特性简介
1.1 接口的静态特性与动态特性
接口是 Go 这门静态语言中唯一“动静兼备”的语法特性。而且,接口“动静兼备”的特性给 Go 带来了强大的表达能力。
接口的静态特性体现在接口类型变量具有静态类型。
比如 var err error 中变量 err 的静态类型为 error。拥有静态类型,那就意味着编译器会在编译阶段对所有接口类型变量的赋值操作进行类型检查,编译器会检查右值的类型是否实现了该接口方法集合中的所有方法。如果不满足,就会报错:
|
|
而接口的动态特性,体现在接口类型变量在运行时还存储了右值的真实类型信息,这个右值的真实类型被称为接口类型变量的动态类型。例如,下面示例代码:
|
|
可以看到,这个示例通过 errros.New 构造了一个错误值,赋值给了 error 接口类型变量 err,并通过 fmt.Printf 函数输出接口类型变量 err 的动态类型为 *errors.errorString。
二、nil error 值不等于 nil
先来看一段改编自GO FAQ 中的例子的代码:
|
|
在这个例子中,我们的关注点集中在 returnsError 这个函数上面。这个函数定义了一个 *MyError 类型的变量 p,初值为 nil。如果函数 bad 返回 false,returnsError 函数就会直接将 p(此时 p = nil)作为返回值返回给调用者,之后调用者会将 returnsError 函数的返回值(error 接口类型)与 nil 进行比较,并根据比较结果做出最终处理。
运行这段程序后,输出如下:
|
|
按照预期:程序执行应该是 p 为 nil,returnsError 返回 p,那么 main 函数中的 err 就等于 nil,于是程序输出 ok 后退出。但是我们看到,示例程序并未按照预期,程序显然是进入了错误处理分支,输出了 err 的值。那这里就有一个问题了:明明 returnsError 函数返回的 p 值为 nil,为什么却满足了 if err != nil 的条件进入错误处理分支呢?
为了弄清楚这个问题,我们来了解接口类型变量的内部表示。
三、interface原理
3.1 interface源代码解析
接口类型“动静兼备”的特性也决定了它的变量的内部表示绝不像一个静态类型变量(如 int、float64)那样简单,在 $GOROOT/src/runtime/runtime2.go 中找到接口类型变量在运行时的表示:
|
|
可以看到,在运行时层面,接口类型变量有两种内部表示:iface 和 eface,这两种表示分别用于不同的接口类型变量:
eface用于表示没有方法的空接口(empty interface)类型变量,也就是interface{}类型的变量;iface用于表示其余拥有方法的接口interface类型变量。
这两个结构的共同点是它们都有两个指针字段,并且第二个指针字段的功能相同,都是指向当前赋值给该接口类型变量的动态类型变量的值。
它们的不同点在于 eface 表示的空接口类型并没有方法列表,因此它的第一个指针字段指向一个 _type 类型结构,这个结构为该接口类型变量的动态类型的信息,它的定义是这样的:
|
|
而 iface 除了要存储动态类型信息之外,还要存储接口本身的信息(接口的类型信息、方法列表信息等)以及动态类型所实现的方法的信息,因此 iface 的第一个字段指向一个 itab 类型结构。itab 结构的定义如下:
|
|
这里可以看到,itab 结构中的第一个字段 inter 指向的 interfacetype 结构,该结构体存储着这个接口类型自身的信息。看下面这段代码表示的 interfacetype 类型定义,这个 interfacetype 结构由类型信息(typ)、包路径名(pkgpath)和接口方法集合切片(mhdr)组成:
|
|
itab 结构中的字段 _type 则存储着这个接口类型变量的动态类型的信息,字段 fun 则是动态类型已实现的接口方法的调用地址数组。
下面我们再结合例子用图片来直观展现 eface 和 iface 的结构。首先我们看一个用 eface 表示的空接口类型变量的例子:
|
|
这个例子中的空接口类型变量 ei 在 Go 运行时的表示是这样的:
可以看到空接口类型的表示较为简单,图中上半部分 _type 字段指向它的动态类型 T 的类型信息,下半部分的 data 则是指向一个 T 类型的实例值。
再来看一个更复杂的用 iface 表示非空接口类型变量的例子:
|
|
和 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。
也就是说,在判断两个接口类型变量是否相等,只需判断 _type/tab 以及 data 是否都相等即可。两个接口变量的 _type/tab 不同时,即两个接口变量的动态类型不相同时,两个接口类型变量一定不等。
当两个接口变量的 _type/tab 相同时,对 data 的相等判断要有区分。当接口变量的动态类型为指针类型时 (*T),Go 不会再额外分配内存存储指针值,而会将动态类型的指针值直接存入 data 字段中,这样 data 值的相等性决定了两个接口类型变量是否相等;当接口变量的动态类型为非指针类型 (T) 时,判断的将不是 data 指针的值是否相等,而是判断 data 指针指向的内存空间所存储的数据值是否相等,若相等,则两个接口类型变量相等。
不过,通过肉眼去辨别接口类型变量是否相等总是困难一些,可以引入一些 helper 函数。借助这些函数,可以清晰地输出接口类型变量的内部表示,这样就可以一目了然地看出两个变量是否相等了。
由于 eface 和 iface 是 runtime 包中的非导出结构体定义,不能直接在包外使用,所以也就无法直接访问到两个结构体中的数据。不过,Go 语言提供了 println 预定义函数,可以用来输出 eface 或 iface 的两个指针字段的值。
在编译阶段,编译器会根据要输出的参数的类型将 println 替换为特定的函数,这些函数都定义在 $GOROOT/src/runtime/print.go 文件中,而针对 eface 和 iface 类型的打印函数实现如下:
|
|
可以看到,printeface 和 printiface 会输出各自的两个指针字段的值。下面我们就来使用 println 函数输出各类接口类型变量的内部表示信息,并结合输出结果,解析接口类型变量的等值比较操作。
四、各类接口类型变量值相等的判断案例
4.1 nil接口变量案例
我们知道,未赋初值的接口类型变量的值为 nil,这类变量也就是 nil 接口变量,我们来看这类变量的内部表示输出的例子:
|
|
运行这个函数,输出结果是这样的:
|
|
可以看到,无论是空接口类型还是非空接口类型变量,一旦变量值为 nil,那么它们内部表示均为 (0x0, 0x0),也就是类型信息、数据值信息均为空。因此上面的变量 i 和 err 等值判断为 true。
4.2 空接口类型变量案例
|
|
首先,代码执行到第 11 行时,eif1 与 eif2 已经分别被赋值整型值 17 与 18,这样 eif1 和 eif2 的动态类型的类型信息是相同的(都是 0x10ac580),但 data 指针指向的内存块中存储的值不同,一个是 17,一个是 18,于是 eif1 不等于 eif2。
接着,代码执行到第 16 行的时候,eif2 已经被重新赋值为 17,这样 eif1 和 eif2 不仅存储的动态类型的类型信息是相同的(都是 0x10ac580),data 指针指向的内存块中存储值也相同了,都是 17,于是 eif1 等于 eif2。
然后,代码执行到第 21 行时,eif2 已经被重新赋值了 int64 类型的数值 17。这样,eif1 和 eif2 存储的动态类型的类型信息就变成不同的了,一个是 int,一个是 int64,即便 data 指针指向的内存块中存储值是相同的,最终 eif1 与 eif2 也是不相等的。
4.3 非空接口类型变量案例
|
|
上面示例中每一轮通过 println 输出的 err1 和 err2 的 tab 和 data 值,要么 data 值不同,要么 tab 与 data 值都不同。
和空接口类型变量一样,只有 tab 和 data 指的数据内容一致的情况下,两个非空接口类型变量之间才能划等号。这里我们要注意 err1 下面的赋值情况:
|
|
针对这种赋值,println 输出的 err1 是(0x10ed120, 0x0),也就是非空接口类型变量的类型信息并不为空,数据指针为空,因此它与 nil(0x0, 0x0)之间不能划等号。
现在再回到开头的那个问题,中,从 returnsError 返回的 error 接口类型变量 err 的数据指针虽然为空,但它的类型信息(iface.tab)并不为空,而是 *MyError 对应的类型信息,这样 err 与 nil(0x0,0x0)相比自然不相等,这就是我们开头那个问题的答案解析。