现象

在日常开发中,可能一不小心就会掉进 Go 语言的某些陷阱里,而本文要介绍的 nil ≠ nil 问题,便是其中一个,初看起来会让人觉得很诡异,摸不着头脑。

先来看个例子:

type CustomizedError struct {
ErrorCode int
Msg string
} func (e *CustomizedError) Error() string {
return fmt.Sprintf("err code: %d, msg: %s", e.ErrorCode, e.Msg)
}
func main() {
txn, err := startTx()
if err != nil {
log.Fatalf("err starting tx: %v", err)
} if err = txn.doUpdate(); err != nil {
log.Fatalf("err updating: %v", err)
} if err = txn.commit(); err != nil {
log.Fatalf("err committing: %v", err)
}
fmt.Println("success!")
} type tx struct{} func startTx() (*tx, error) {
return &tx{}, nil
} func (*tx) doUpdate() *CustomizedError {
return nil
} func (*tx) commit() error {
return nil
}

这是一个简化过了的例子,在上述代码中,我们创建了一个事务,然后做了一些更新,在更新过程中如果发生了错误,希望返回对应的错误码和提示信息。

如果感兴趣的话,可以在这个地址在线运行这份代码:

Go Playground - The Go Programming Language

看起来每个方法都会返回 nil,应该能顺利走到最后一行,输出 success 才对,但实际上,输出的却是:

err updating: <nil>

寻找原因

为什么明明返回的是 nil,却被判定为 err ≠ nil 呢?难道这个 nil 也有什么奇妙之处?

这就需要我们来更深入一点了解 error 本身了。在 Go 语言中, error 是一个 interface ,内部含有一个 Error() 函数,返回一个字符串,接口的描述如下:

// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
type error interface {
Error() string
}

而对于一个变量来说,它有两个要素,一个是 type T,一个是 value V,如下图所示:

来看一个简单的例子:

var it interface{}
fmt.Println(reflect.TypeOf(it), reflect.ValueOf(it)) // <nil> <invalid reflect.Value>
it = 1
fmt.Println(reflect.TypeOf(it), reflect.ValueOf(it)) // int 1
it = "hello"
fmt.Println(reflect.TypeOf(it), reflect.ValueOf(it)) // string hello
var s *string
it = s
fmt.Println(reflect.TypeOf(it), reflect.ValueOf(it)) // *string <nil>
ss := "hello"
it = &ss
fmt.Println(reflect.TypeOf(it), reflect.ValueOf(it)) // *string 0xc000096560

在给一个 interface 变量赋值前,TV 都是 nil,但给它赋值后,不仅会改变它的值,还会改变它的类型。

当把一个值为 nil 的字符串指针赋值给它后,虽然它的值是 V=nil,但它的类型 T 却变成了 *string

此时如果拿它来跟 nil 比较,结果就会是不相等,因为只有当这个 interface 变量的类型和值都未被设置时,它才真正等于 nil

再来看看之前的例子中,err 变量的 TV 是如何变化的:

func main() {
txn, err := startTx()
fmt.Println(reflect.TypeOf(err), reflect.ValueOf(err))
if err != nil {
log.Fatalf("err starting tx: %v", err)
} if err = txn.doUpdate(); err != nil {
fmt.Println(reflect.TypeOf(err), reflect.ValueOf(err))
log.Fatalf("err updating: %v", err)
} if err = txn.commit(); err != nil {
log.Fatalf("err committing: %v", err)
}
fmt.Println("success!")
}

输出如下:

<nil> <invalid reflect.Value>
*err.CustomizedError <nil>

在一开始,我们给 err 初始化赋值时,startTx 函数返回的是一个 error 接口类型的 nil。此时查看其类型 T 和值 V 时,都会是 nil

txn, err := startTx()
fmt.Println(reflect.TypeOf(err), reflect.ValueOf(err)) // <nil> <invalid reflect.Value> func startTx() (*tx, error) {
return &tx{}, nil
}

而在调用 doUpdate 时,会将一个 *CustomizedError 类型的 nil 值赋值给了它,它的类型 T 便成了 *CustomizedError ,V 是 nil

err = txn.doUpdate()
fmt.Println(reflect.TypeOf(err), reflect.ValueOf(err)) // *err.CustomizedError <nil>

所以在做 err ≠ nil 的比较时,err 的类型 T 已经不是 nil,前面已经说过,只有当一个接口变量的 TV 同时为 nil 时,这个变量才会被判定为 nil,所以该不等式会判定为 true

要修复这个问题,其实最简单的方法便是在调用 doUpdate 方法时给 err 进行重新声明:

if err := txn.doUpdate(); err != nil {
log.Fatalf("err updating: %v", err)
}

此时,err 其实成了一个新的结构体指针变量,而不再是一个interface 类型变量,类型为 *CustomizedError ,且值为 nil,所以做 err ≠ nil 的比较时结果就是将是 false

问题到这里似乎就告一段落了,但,再仔细想想,就会发现这其中似乎还是漏掉了一环。

如果给一个 interface 类型的变量赋值时,会同时改变它的类型 T 和值 V,那跟 nil 比较时为什么不是跟它的新类型对应的 nil 比较呢?

事实上,interface 变量跟普通变量确实有一定区别,一个非空接口 interface (即接口中存在函数方法)初始化的底层数据结构是 iface,一个空接口变量对应的底层结构体为 eface

type iface struct {
tab *itab
data unsafe.Pointer
} type eface struct {
_type *_type
data unsafe.Pointer
}

tab 中存放的是类型、方法等信息。data 指针指向的 iface 绑定对象的原始数据的副本。

再来看一下 itab 的结构:

// layout of Itab known to compilers
// allocated in non-garbage-collected memory
// Needs to be in sync with
// ../cmd/compile/internal/reflectdata/reflect.go:/^func.WriteTabs.
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 中一共包含 5 个字段,inner 字段存的是初始化 interface 时的静态类型。_type 存的是 interface 对应具体对象的类型,当 interface 变量被赋值后,这个字段便会变成被赋值的对象的类型。

itab 中的 _typeiface 中的 data 便分别对应 interface 变量的 TV_type 是这个变量对应的类型,data 是这个变量的值。在之前的赋值测试中,通过 reflect.TypeOfreflect.ValueOf 方法获取到的信息也分别来自这两个字段。

这里的 hash 字段和 _type 中存的 hash 字段是完全一致的,这么做的目的是为了类型断言。

fun 是一个函数指针,它指向的是具体类型的函数方法,在这个指针对应内存地址的后面依次存储了多个方法,利用指针偏移便可以找到它们。

再来看看 interfacetype 的结构:

type interfacetype struct {
typ _type
pkgpath name
mhdr []imethod
}

这其中也有一个 _type 字段,来表示 interface 变量的初始类型。

看到这里,之前的疑问便开始清晰起来,一个 interface 变量实际上有两个类型,一个是初始化时赋值时对应的 interface 类型,一个是赋值具体对象时,对象的实际类型。

了解了这些之后,我们再来看一下之前的例子:

txn, err := startTx()

这里先对 err 进行初始化赋值,此时,它的 itab.inter.typ 对应的类型信息就是 error itab._type 仍为 nil

err = txn.doUpdate()

当对 err 进行重新赋值时,erritab._type 字段会被赋值成 *CustomizedError ,所以此时,err 变量实际上是一个 itab.inter.typerror ,但实际类型为 *CustomizedError ,值为 nil 的接口变量。

把一个具体类型变量与 nil 比较时,只需要判断其 value 是否为 nil 即可,而把一个接口类型的变量与 nil 进行比较时,还需要判断其类型 itab._type 是否为nil

如果想实际看看被赋值后 err 对应的 iface 结构,可以把 iface 相关的结构体都复制到同一个包下,然后通过 unsafe.Pointer 进行类型强转,就可以通过打断点的方式来查看了。

func TestErr(t *testing.T) {
txn, err := startTx()
fmt.Println(reflect.TypeOf(err), reflect.ValueOf(err))
if err != nil {
log.Fatalf("err starting tx: %v", err)
} p := (*iface)(unsafe.Pointer(&err))
fmt.Println(p.data) if err = txn.doUpdate(); err != nil {
fmt.Println(reflect.TypeOf(err), reflect.ValueOf(err))
p := (*iface)(unsafe.Pointer(&err))
fmt.Println(p.data)
log.Fatalf("err updating: %v", err)
} if err = txn.commit(); err != nil {
log.Fatalf("err committing: %v", err)
}
fmt.Println("success!")
}

补充说明一下,这里的inter.typ.kind 表示的是变量的基本类型,其值对应 runtime 包下的枚举。

const (
kindBool = 1 + iota
kindInt
kindInt8
kindInt16
kindInt32
kindInt64
kindUint
kindUint8
kindUint16
kindUint32
kindUint64
kindUintptr
kindFloat32
kindFloat64
kindComplex64
kindComplex128
kindArray
kindChan
kindFunc
kindInterface
kindMap
kindPtr
kindSlice
kindString
kindStruct
kindUnsafePointer kindDirectIface = 1 << 5
kindGCProg = 1 << 6
kindMask = (1 << 5) - 1
)

比如上图中所示的 kind = 20 对应的类型就是 kindInterface

总结

  1. 接口类型变量跟普通变量是有差异的,非空接口类型变量对应的底层结构是 iface ,空接口类型类型变量对应的底层结构是 eface
  2. iface 中有两个跟类型相关的字段,一个表示的是接口的类型 inter,一个表示的是变量实际类型 _type
  3. 只有当接口变量的 itab._type 与 data 都为 nil 时,也就是实际类型和值都未被赋值前,才真正等于 nil

到此,一个有趣的探索之旅就结束了,但长路漫漫,前方还有无数的问题等待我们去探索和发现,这便是学习的乐趣,希望能与君共勉。

[Go疑难杂症]为什么nil不等于nil的更多相关文章

  1. Swift不等于nil

    我照着书上的例子写下了如下代码,运行后发现提示Nil cannot be assigned to type 'Int' if i!=nil {//Nil cannot be assigned to t ...

  2. Delphi中,FALSE 和 nil ,true 和 nil,0的区别

    True和False是布尔型(Boolean)的值,就是"是"或"否"的意思.nil就是空,一般用于指针或对象变量,指对针或对象对象一般初始化为nil或者释放后 ...

  3. Go中error类型的nil值和nil

    https://my.oschina.net/chai2010/blog/117923

  4. 【Go】深入剖析slice和array

    文章来源:https://blog.thinkeridea.com/201901/go/shen_ru_pou_xi_slice_he_array.html array 和 slice 看似相似,却有 ...

  5. nil/Nil/NULL/NSNull

    nil/Nil/NULL/NSNull的区别 一个简单的小例子: NSObject *obj = nil; NSLog(@"%@",obj); =>null NSObject ...

  6. [转]理解Go语言中的nil

    最近在油管上面看了一个视频:Understanding nil,挺有意思,这篇文章就对视频做一个归纳总结,代码示例都是来自于视频. nil是什么 相信写过Golang的程序员对下面一段代码是非常非常熟 ...

  7. [Go] 理解 golang 中的 nil

    nil是什么 相信写过Golang的程序员对下面一段代码是非常非常熟悉的了: if err != nil { // do something.... } 当出现不等于nil的时候,说明出现某些错误了, ...

  8. golang interface判断为空nil

    要判断interface 空的问题,首先看下其底层实现. interface 底层结构 根据 interface 是否包含有 method,底层实现上用两种 struct 来表示:iface 和 ef ...

  9. goalng nil interface浅析

    0.遇到一个问题 代码 func GetMap (i interface{})(map[string]interface{}){ if i == nil { //false ??? i = make( ...

随机推荐

  1. Redis 06 哈希

    参考源 https://www.bilibili.com/video/BV1S54y1R7SB?spm_id_from=333.999.0.0 版本 本文章基于 Redis 6.2.6 哈希就是 ke ...

  2. html页面嵌套其他网站页面的方法

    直接上代码:html页面嵌套其他网站页面的方法 <div> <!--第一种:使用object标签--> <object type="text/html" ...

  3. Dreamweaver8 网站制作软件使用教程

    Dreamweaver是我喜欢做网站的软件.之所以喜欢Dreamweaver 8 是因为这个版本有折叠功能. 下面说说它的使用方法. 1.创建站点文件.注意看截图的顺序,如下图 2.选择你喜欢的编辑模 ...

  4. 【java】学习路线7-继承、super方法、重写、重载

    /*继承-java只有单继承如果你创建了很多个class,但是之间有很多相同的成员变量和成员方法,修改的时候又要多处修改好麻烦,此时就可以创建多一个类来存储这些重复的东西,统一管理.相当方便.*//* ...

  5. 项目实践2:(问卷)用html和css做一个网页

    好家伙,又来写项目了 1.以下是考题,姑且把他理解为甲方吧. 2.以下是附带的题目素材 开干.

  6. 免费内网穿透服务Localtunnel

    Localtunnel 将为您分配一个唯一的可公开访问的 url,它将所有请求代理到您本地运行的网络服务器. 快速开始 全局安装 Localtunnel(需要 NodeJS)以使其在任何地方都可以访问 ...

  7. 【Android 逆向】ARM CPP 类对象

    #include <stdio.h> class aclass{ private: int m; char c; public: aclass(int i, char ch) { prin ...

  8. React版/Vue版都齐了,开源一套【特别】的后台管理系统...

    本项目主要基于Elux+Antd构建,包含React版本和Vue版本,旨在提供给大家一个简单基础.开箱即用的后台管理系统通用模版,主要包含运行环境.脚手架.代码风格.基本Layout.状态管理.路由管 ...

  9. docker-compose部署elastiflow

    docker-compose导入导出命令 涉及的命令有export.import.save.load save 命令 docker save [options] images [images...] ...

  10. 企业运维实践-丢弃手中的 docker build , 使用Kaniko直接在Kubernetes集群或Containerd环境中快速进行构建推送容器镜像

    关注「WeiyiGeek」公众号 设为「特别关注」每天带你玩转网络安全运维.应用开发.物联网IOT学习! 希望各位看友[关注.点赞.评论.收藏.投币],助力每一个梦想. 本章目录 目录 首发地址: h ...