[Go疑难杂症]为什么nil不等于nil
现象
在日常开发中,可能一不小心就会掉进 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
变量赋值前,T
和 V
都是 nil
,但给它赋值后,不仅会改变它的值,还会改变它的类型。
当把一个值为 nil
的字符串指针赋值给它后,虽然它的值是 V=nil
,但它的类型 T
却变成了 *string
。
此时如果拿它来跟 nil
比较,结果就会是不相等,因为只有当这个 interface
变量的类型和值都未被设置时,它才真正等于 nil
。
再来看看之前的例子中,err
变量的 T
和 V
是如何变化的:
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
,前面已经说过,只有当一个接口变量的 T
和 V
同时为 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
中的 _type
和 iface
中的 data
便分别对应 interface
变量的 T
和 V
,_type
是这个变量对应的类型,data
是这个变量的值。在之前的赋值测试中,通过 reflect.TypeOf
与 reflect.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
进行重新赋值时,err
的 itab._type
字段会被赋值成 *CustomizedError
,所以此时,err
变量实际上是一个 itab.inter.typ
为 error
,但实际类型为 *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
。
总结
- 接口类型变量跟普通变量是有差异的,非空接口类型变量对应的底层结构是
iface
,空接口类型类型变量对应的底层结构是eface
。 iface
中有两个跟类型相关的字段,一个表示的是接口的类型i
nter,一个表示的是变量实际类型_type
。- 只有当接口变量的
itab._type
与 data 都为nil
时,也就是实际类型和值都未被赋值前,才真正等于nil
。
到此,一个有趣的探索之旅就结束了,但长路漫漫,前方还有无数的问题等待我们去探索和发现,这便是学习的乐趣,希望能与君共勉。
[Go疑难杂症]为什么nil不等于nil的更多相关文章
- Swift不等于nil
我照着书上的例子写下了如下代码,运行后发现提示Nil cannot be assigned to type 'Int' if i!=nil {//Nil cannot be assigned to t ...
- Delphi中,FALSE 和 nil ,true 和 nil,0的区别
True和False是布尔型(Boolean)的值,就是"是"或"否"的意思.nil就是空,一般用于指针或对象变量,指对针或对象对象一般初始化为nil或者释放后 ...
- Go中error类型的nil值和nil
https://my.oschina.net/chai2010/blog/117923
- 【Go】深入剖析slice和array
文章来源:https://blog.thinkeridea.com/201901/go/shen_ru_pou_xi_slice_he_array.html array 和 slice 看似相似,却有 ...
- nil/Nil/NULL/NSNull
nil/Nil/NULL/NSNull的区别 一个简单的小例子: NSObject *obj = nil; NSLog(@"%@",obj); =>null NSObject ...
- [转]理解Go语言中的nil
最近在油管上面看了一个视频:Understanding nil,挺有意思,这篇文章就对视频做一个归纳总结,代码示例都是来自于视频. nil是什么 相信写过Golang的程序员对下面一段代码是非常非常熟 ...
- [Go] 理解 golang 中的 nil
nil是什么 相信写过Golang的程序员对下面一段代码是非常非常熟悉的了: if err != nil { // do something.... } 当出现不等于nil的时候,说明出现某些错误了, ...
- golang interface判断为空nil
要判断interface 空的问题,首先看下其底层实现. interface 底层结构 根据 interface 是否包含有 method,底层实现上用两种 struct 来表示:iface 和 ef ...
- goalng nil interface浅析
0.遇到一个问题 代码 func GetMap (i interface{})(map[string]interface{}){ if i == nil { //false ??? i = make( ...
随机推荐
- Filter中的FilterChain.doFilter(req,resp)的报错解决
服务器内部错误:500 Request processing failed; nested exception is java.lang.IllegalStateException: 提交响应后无法调 ...
- Kotlin快速上手
一.Kotlin基础 1.数据类型声明 在Kotlin中要定义一个变量需要使用var关键字 //定义了一个可以修改的Int类型变量 var number = 39 如果要定义一个常量可以使用val关键 ...
- ak日记 831 dxm
import sys from math import inf line = sys.stdin.readline().strip() vs = list(map(int, line.split()) ...
- 第五十一篇:webpack中的loader(二) --less-loader
好家伙 先扩充一下知识点: 什么是.less文件? 作为一名前端开发的同学,很多时候我们都无法避免地要去写大量的 CSS 代码, 而且耗费的时间还不少,所以学习一种能够提升开发效率的 CSS 预处理器 ...
- 开源:Taurus.MVC-Java 版本框架 (支持javax.servlet.*和jakarta.servlet.*双系列,内集成微服务客户端)
版本说明: 因为之前有了Taurus.MVC-DotNet 版本框架,因此框架标了-Java后缀. .Net 版本: 开源文章:开源:Taurus.MVC-DotNet 版本框架 (支持.NET C ...
- 第十章 Kubernetes的CNI网络插件--flannel
1.简介 1.1前言 Kubernetes设计了网络模型,但却将它的实现讲给了网络插件,CNI网络插件最重要的功能就是实现Pod资源能够跨主机通信 常见的CNI网络插件如下: Flannel: Cac ...
- KingbaseES V8R6集群部署案例之---Windows环境配置主备流复制(异机复制)
案例说明: 目前KingbaseES V8R6的Windows版本不支持数据库sys_rman的物理备份,可以考虑通过建立主备流复制实现数据库的异机物理备份.本案例详细介绍了,在Windows环境下建 ...
- 如何查找并简单分析core文件
当系统发生coredump时,通常需要通过分析core文件来定位问题所在,但实际工作中,有时却发现core 文件找不到,或者core文件被删除了. 一.core文件没有生成 KINGBASE core ...
- KingbaseES 数据库软件卸载
关键字: KingbaseES.卸载 一.安装后检查 在安装完成后,可以通过以下几种方式进行安装正确性验证: 1. 查看安装日志,确认没有错误记录; 2. 查看开始菜单: 查看应用程序菜单中是否安 ...
- C++ 指针与二维数组名
和一维数组类似,C++ 将二维数组名解释为其第一个元素的地址,而二维数组的第一个元素为一维数组,以下面的程序为例,二维数组名 array2d 和 &array2d[0] 等效,它们的类型都为 ...