一、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)相比自然不相等,这就是我们开头那个问题的答案解析。