Go疑难杂症讲解之为什么nil不等于nil
在Golang实战开发的过程中,我们经常会遇到一些这样那样的问题,然后要卡好半天,等问题解决了才发现原来一些细节知识点还是没有掌握好。今天golang学习网就整理分享《Go疑难杂症讲解之为什么nil不等于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,却被判定为 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
</nil></invalid></nil>
在给一个 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!")
}
输出如下:
*err.CustomizedError
在一开始,我们给 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
}</invalid></nil>
而在调用 doUpdate 时,会将一个 *CustomizedError 类型的 nil 值赋值给了它,它的类型 T 便成了 *CustomizedError ,V 是 nil。
err = txn.doUpdate() fmt.Println(reflect.TypeOf(err), reflect.ValueOf(err)) // *err.CustomizedError <nil></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 <p>比如上图中所示的 <code>kind = 20</code> 对应的类型就是 <code>kindInterface</code>。</p> <h2>总结</h2>
- 接口类型变量跟普通变量是有差异的,非空接口类型变量对应的底层结构是
iface,空接口类型类型变量对应的底层结构是eface。 iface中有两个跟类型相关的字段,一个表示的是接口的类型inter,一个表示的是变量实际类型_type。- 只有当接口变量的
itab._type与 data 都为nil时,也就是实际类型和值都未被赋值前,才真正等于nil。
到这里,我们也就讲完了《Go疑难杂症讲解之为什么nil不等于nil》的内容了。个人认为,基础知识的学习和巩固,是为了更好的将其运用到项目中,欢迎关注golang学习网公众号,带你了解更多关于golang的知识点!
基于Go语言实现类似tree命令的小程序
- 上一篇
- 基于Go语言实现类似tree命令的小程序
- 下一篇
- Go语言实战之实现一个简单分布式系统
-
- Golang · Go教程 | 6小时前 | 格式化输出 printf fmt库 格式化动词 Stringer接口
- Golangfmt库用法与格式化技巧解析
- 140浏览 收藏
-
- Golang · Go教程 | 6小时前 |
- Golang配置Protobuf安装教程
- 147浏览 收藏
-
- Golang · Go教程 | 6小时前 |
- Golang中介者模式实现与通信解耦技巧
- 378浏览 收藏
-
- Golang · Go教程 | 7小时前 |
- Golang多协程通信技巧分享
- 255浏览 收藏
-
- Golang · Go教程 | 7小时前 |
- Golang如何判断变量类型?
- 393浏览 收藏
-
- Golang · Go教程 | 7小时前 |
- Golang云原生微服务实战教程
- 310浏览 收藏
-
- Golang · Go教程 | 8小时前 |
- Golang迭代器与懒加载结合应用
- 110浏览 收藏
-
- Golang · Go教程 | 8小时前 | 性能优化 并发安全 Golangslicemap 预设容量 指针拷贝
- Golangslicemap优化技巧分享
- 412浏览 收藏
-
- Golang · Go教程 | 8小时前 |
- Golang代理模式与访问控制实现解析
- 423浏览 收藏
-
- Golang · Go教程 | 8小时前 |
- Golang事件管理模块实现教程
- 274浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- ChatExcel酷表
- ChatExcel酷表是由北京大学团队打造的Excel聊天机器人,用自然语言操控表格,简化数据处理,告别繁琐操作,提升工作效率!适用于学生、上班族及政府人员。
- 3166次使用
-
- Any绘本
- 探索Any绘本(anypicturebook.com/zh),一款开源免费的AI绘本创作工具,基于Google Gemini与Flux AI模型,让您轻松创作个性化绘本。适用于家庭、教育、创作等多种场景,零门槛,高自由度,技术透明,本地可控。
- 3379次使用
-
- 可赞AI
- 可赞AI,AI驱动的办公可视化智能工具,助您轻松实现文本与可视化元素高效转化。无论是智能文档生成、多格式文本解析,还是一键生成专业图表、脑图、知识卡片,可赞AI都能让信息处理更清晰高效。覆盖数据汇报、会议纪要、内容营销等全场景,大幅提升办公效率,降低专业门槛,是您提升工作效率的得力助手。
- 3408次使用
-
- 星月写作
- 星月写作是国内首款聚焦中文网络小说创作的AI辅助工具,解决网文作者从构思到变现的全流程痛点。AI扫榜、专属模板、全链路适配,助力新人快速上手,资深作者效率倍增。
- 4512次使用
-
- MagicLight
- MagicLight.ai是全球首款叙事驱动型AI动画视频创作平台,专注于解决从故事想法到完整动画的全流程痛点。它通过自研AI模型,保障角色、风格、场景高度一致性,让零动画经验者也能高效产出专业级叙事内容。广泛适用于独立创作者、动画工作室、教育机构及企业营销,助您轻松实现创意落地与商业化。
- 3788次使用
-
- Go疑难杂症讲解之为什么nil不等于nil
- 2022-12-22 374浏览
-
- golang中的nil接收器详解
- 2023-01-07 355浏览
-
- 基于go interface{}==nil 的几种坑及原理分析
- 2022-12-23 304浏览
-
- golang interface判断为空nil的实现代码
- 2023-01-07 113浏览
-
- golang 中的 nil的场景分析
- 2023-02-23 489浏览

