Go 接口:nil接口为什么不等于nil?
发表于 ・ 资讯中心
一、前言
Go 语言核心团队的技术负责人 Russ Cox 也曾说过这样一句话:“如果要从 Go 语言中挑选出一个特性放入其他语言,我会选择接口”,这句话足以说明接口这一语法特性在这位 Go 语言大神心目中的地位。
为什么接口在 Go 中有这么高的地位呢?这是因为接口是 Go 这门静态语言中唯一“动静兼备”的语法特性。而且,接口“动静兼备”的特性给 Go 带来了强大的表达能力,但同时也给 Go 语言初学者带来了不少困惑。要想真正解决这些困惑,我们必须深入到 Go 运行时层面,看看 Go 语言在运行时是如何表示接口类型的。
接下来,我们先来看看接口的静态与动态特性,看看“动静皆备”的含义。
1.1 接口的静态特性与动态特性
接口的静态特性体现在接口类型变量具有静态类型。
var err error = 1 // cannot use 1 (type int) as type error in assignment: int does not implement error (missing Error method)var err errorerr = errors.New("error1")fmt.Printf("%T\n", err) // *errors.errorString
我们可以看到,这个示例通过 errros.New 构造了一个错误值,赋值给了 error 接口类型变量 err,并通过 fmt.Printf 函数输出接口类型变量 err 的动态类型为 *errors.errorString。
👉点击领取 Go后端开发资料合集
二、nil error 值 != nil
我们先来看一段改编自GO FAQ 中的例子的代码:
type MyError struct {error}var ErrBad = MyError{error: errors.New("bad things happened"),}func bad() bool {return false}func returnsError() error {var p *MyError = nilif bad() {p = &ErrBad}return p}func main() {err := returnsError()if err != nil {fmt.Printf("error occur: %+v\n", err)return}fmt.Println("ok")}
在这个例子中,我们的关注点集中在 returnsError 这个函数上面。这个函数定义了一个 *MyError 类型的变量 p,初值为 nil。如果函数 bad 返回 false,returnsError 函数就会直接将 p(此时 p = nil)作为返回值返回给调用者,之后调用者会将 returnsError 函数的返回值(error 接口类型)与 nil 进行比较,并根据比较结果做出最终处理。
error occur: <nil>按照预期:程序执行应该是p 为 nil,returnsError 返回 p,那么 main 函数中的 err 就等于 nil,于是程序输出 ok 后退出。但是我们看到,示例程序并未按照预期,程序显然是进入了错误处理分支,输出了 err 的值。那这里就有一个问题了:明明 returnsError 函数返回的 p 值为 nil,为什么却满足了 if err != nil 的条件进入错误处理分支呢?
为了弄清楚这个问题,我们来了解接口类型变量的内部表示。
2.1 interface源代码解析
// $GOROOT/src/runtime/runtime2.gotype iface struct {tab *itabdata unsafe.Pointer}type eface struct {_type *_typedata unsafe.Pointer}
我们看到,在运行时层面,接口类型变量有两种内部表示:iface 和 eface,这两种表示分别用于不同的接口类型变量:
eface 用于表示没有方法的空接口(empty interface)类型变量,也就是 interface{} 类型的变量;
iface 用于表示其余拥有方法的接口 interface 类型变量。
这两个结构的共同点是它们都有两个指针字段,并且第二个指针字段的功能相同,都是指向当前赋值给该接口类型变量的动态类型变量的值。
// $GOROOT/src/runtime/type.gotype _type struct {size uintptrptrdata uintptr // size of memory prefix holding all pointershash uint32tflag tflagalign uint8fieldAlign uint8kind 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 *bytestr nameOffptrToThis typeOff}
// $GOROOT/src/runtime/runtime2.gotype itab struct {inter *interfacetype_type *_typehash uint32 // copy of _type.hash. Used for type switches._ [4]bytefun [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.}
// $GOROOT/src/runtime/type.gotype interfacetype struct {typ _typepkgpath namemhdr []imethod}
itab 结构中的字段 _type 则存储着这个接口类型变量的动态类型的信息,字段 fun 则是动态类型已实现的接口方法的调用地址数组。
type T struct {n ints 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 ints 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。
也就是说,我们判断两个接口类型变量是否相等,只需判断 _type/tab 以及 data 是否都相等即可。两个接口变量的 _type/tab 不同时,即两个接口变量的动态类型不相同时,两个接口类型变量一定不等。
当两个接口变量的 _type/tab 相同时,对 data 的相等判断要有区分。当接口变量的动态类型为指针类型时 (*T),Go 不会再额外分配内存存储指针值,而会将动态类型的指针值直接存入 data 字段中,这样 data 值的相等性决定了两个接口类型变量是否相等;当接口变量的动态类型为非指针类型 (T) 时,我们判断的将不是 data 指针的值是否相等,而是判断 data 指针指向的内存空间所存储的数据值是否相等,若相等,则两个接口类型变量相等。
不过,通过肉眼去辨别接口类型变量是否相等总是困难一些,我们可以引入一些 helper 函数。借助这些函数,我们可以清晰地输出接口类型变量的内部表示,这样就可以一目了然地看出两个变量是否相等了。
由于 eface 和 iface 是 runtime 包中的非导出结构体定义,我们不能直接在包外使用,所以也就无法直接访问到两个结构体中的数据。不过,Go 语言提供了 println 预定义函数,可以用来输出 eface 或 iface 的两个指针字段的值。
// $GOROOT/src/runtime/print.gofunc printeface(e eface) {print("(", e._type, ",", e.data, ")")}func printiface(i iface) {print("(", i.tab, ",", i.data, ")")}
我们看到,printeface 和 printiface 会输出各自的两个指针字段的值。下面我们就来使用 println 函数输出各类接口类型变量的内部表示信息,并结合输出结果,解析接口类型变量的等值比较操作。
2.2 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: trueerr = nil: truei = err: true
我们看到,无论是空接口类型还是非空接口类型变量,一旦变量值为 nil,那么它们内部表示均为 (0x0, 0x0),也就是类型信息、数据值信息均为空。因此上面的变量 i 和 err 等值判断为 true。
func printEmptyInterface() {var eif1 interface{} // 空接口类型var eif2 interface{} // 空接口类型var n, m int = 17, 18eif1 = neif2 = mprintln("eif1:", eif1)println("eif2:", eif2)println("eif1 = eif2:", eif1 == eif2) // falseeif2 = 17println("eif1:", eif1)println("eif2:", eif2)println("eif1 = eif2:", eif1 == eif2) // trueeif2 = int64(17)println("eif1:", eif1)println("eif2:", eif2)println("eif1 = eif2:", eif1 == eif2) // false}
eif1: (0x10ac580,0xc00007ef48)eif2: (0x10ac580,0xc00007ef40)eif1 = eif2: falseeif1: (0x10ac580,0xc00007ef48)eif2: (0x10ac580,0x10eb3d0)eif1 = eif2: trueeif1: (0x10ac580,0xc00007ef48)eif2: (0x10ac640,0x10eb3d8)eif1 = eif2: false
首先,代码执行到第 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 也是不相等的。
2.4 非空接口类型变量案例
type T intfunc (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)}
我们看到上面示例中每一轮通过 println 输出的 err1 和 err2 的 tab 和 data 值,要么 data 值不同,要么 tab 与 data 值都不同。
err1 = (*T)(nil)针对这种赋值,println 输出的 err1 是(0x10ed120, 0x0),也就是非空接口类型变量的类型信息并不为空,数据指针为空,因此它与 nil(0x0, 0x0)之间不能划等号。
![]()
热门推荐

发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。