当前位置: 首页 > news >正文

[Go疑难杂症]为什么nil不等于nil

现象

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

先来看个例子:

type CustomizedError struct {ErrorCode intMsg       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
}

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

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

err updating: <nil>

寻找原因

为什么明明返回的是 nil,却被判定为 err ≠ nil 呢?难道这个 nil 也有什么奇妙之处?
这就需要我们来更深入一点了解 error 本身了。在 Go 语言中, error 是一个 interface ,内部含有一个 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  *itabdata unsafe.Pointer
}type eface struct {_type *_typedata  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 *_typehash  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     _typepkgpath namemhdr    []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 + iotakindIntkindInt8kindInt16kindInt32kindInt64kindUintkindUint8kindUint16kindUint32kindUint64kindUintptrkindFloat32kindFloat64kindComplex64kindComplex128kindArraykindChankindFunckindInterfacekindMapkindPtrkindSlicekindStringkindStructkindUnsafePointerkindDirectIface = 1 << 5kindGCProg      = 1 << 6kindMask        = (1 << 5) - 1
)

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

总结

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

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

相关文章:

[Go疑难杂症]为什么nil不等于nil

现象 在日常开发中&#xff0c;可能一不小心就会掉进 Go 语言的某些陷阱里&#xff0c;而本文要介绍的 nil ≠ nil 问题&#xff0c;便是其中一个&#xff0c;初看起来会让人觉得很诡异&#xff0c;摸不着头脑。 先来看个例子&#xff1a; type CustomizedError struct {Err…...

C#60个常见的问题和答案

在本文中,我将帮助你准备好在下一次面试中解决这些与C# 编程语言相关的问题。同时,你可能想练习一些C# 项目。这 60 个基本的 C#面试问题和答案将帮助你了解该语言的技术概念。 目录 什么是 C#? 1.什么是类? 2.面向对象编程的主要概念是什么?...

11:STM32---spl通信

目录 一:SPL通信 1:简历 2:硬件电路 3:移动数据图 4:SPI时序基本单元 A : 开/ 终条件 B:SPI时序基本单元 A:模式0 B:模式1 C:模式2 D:模式3 C:SPl时序 A:发送指令 B: 指定地址写 C:指定地址读 二: W25Q64 1:简历 2: 硬件电路 3:W25Q64框图 4: Flash操作注意…...

kafka的 ack 应答机制

目录 一 ack 应答机制 二 ISR 集合 一 ack 应答机制 kafka 为用户提供了三种应答级别&#xff1a; all&#xff0c;leader&#xff0c;0 acks &#xff1a;0 这一操作提供了一个最低的延迟&#xff0c;partition的leader接收到消息还没有写入磁盘就已经返回ack&#x…...

Django系列:Django开发环境配置与第一个Django项目

Django系列 Django开发环境配置与第一个Django项目 作者&#xff1a;李俊才 &#xff08;jcLee95&#xff09;&#xff1a;https://blog.csdn.net/qq_28550263 邮箱 &#xff1a;291148484163.com 本文地址&#xff1a;https://blog.csdn.net/qq_28550263/article/details/1328…...

iPad协议/微信协议最新版

一、了解微信的协议 在开发微信协议之前&#xff0c;需要先了解微信的协议。微信的协议包括登录协议、消息传输协议、文件传输协议、数据同步协议等。其中&#xff0c;登录协议是最重要的协议之一&#xff0c;包括登录验证、登录认证等。消息传输协议则是微信最核心的功能之一…...

URL字符解码

将网页编码文字还原&#xff1a; 例如&#xff1a;https%3A%2F%2Fwww.example.com%2F%3Fparam%3Dvalue%26key%3D%E4%B8%AD%E6%96%87 解码&#xff1a; https: // www.example.com/?paramvalue&key中文 代码&#xff1a; char hexValue(char ch) {if (isdigit(ch)){re…...

uni-app进行表单效验

Uni-app内置了一些表单验证方法&#xff0c;可以帮助我们对表单进行有效的验证。以下是一些常用的验证方法&#xff1a; 非空验证&#xff1a; if(!this.formData.name){uni.showToast({title: 请输入姓名,icon: none});return false; }手机号码验证&#xff1a; const phon…...

IO流内容总结

IO流作用 对文件或者网络中的数据进行读写操作。 简单记&#xff1a;输入流读数据&#xff0c;输出流写数据。 Java的输出流主要以OutputStream和Writer作为基类&#xff0c;输入流主要是以InputStream和Reader作为基类。 按处理数据单元分类 字节流 字节输入流&#xff…...

MySQL的进阶篇1-MySQL的存储引擎简介

存储引擎 MySQL的体系结构 0、客户端连机器【java、Python、JDBC等】 1、【MySQL服务器-连接层】认证&#xff0c;授权&#xff0c;连接池 2、【MySQL服务器-服务层】 {SQL接口&#xff08;DML、DDL、存储过程、触发器&#xff09;、解析器、查询优化器、缓存} 3、【MySQL…...

九芯电子丨语音智能风扇,助您畅享智慧生活

回忆童年时期的传统机械风扇&#xff0c;那“古老”的扇叶连摆动看起来是那么吃力。在一个闷热的夏夜&#xff0c;风扇的噪音往往令人印象深刻。但在今天&#xff0c;静音家用风扇已取代了传统的机械风扇。与此同时&#xff0c;随着智能化的发展&#xff0c;智能家居已逐渐成为…...

2101. 引爆最多的炸弹;752. 打开转盘锁;1234. 替换子串得到平衡字符串

2101. 引爆最多的炸弹 核心思想&#xff1a;枚举BFS。枚举每个炸弹最多引爆多少个炸弹&#xff0c;对每个炸弹进行dfs&#xff0c;一个炸弹能否引爆另一个炸弹是两个炸弹的圆心距离在第一个炸弹的半径之内。 752. 打开转盘锁 核心思想:典型BFS&#xff0c;就像水源扩散一样&a…...

​校园学习《乡村振兴战略下传统村落文化旅游设计》许少辉八一新著

​校园学习《乡村振兴战略下传统村落文化旅游设计》许少辉八一新著...

UOS服务器操作系统搭建离线yum仓库

UOS服务器操作系统搭建离线yum仓库 1050e版本操作系统&#xff08;适用ARM64和AMD64&#xff09;1、挂载everything镜像并同步2、配置本地仓库3、配置nginx发布离线源 1050e版本操作系统&#xff08;适用ARM64和AMD64&#xff09; 首先需要有everything镜像文件 服务端操作流…...

C# 实现数独游戏

1.数独单元 public struct SudokuCell{public SudokuCell() : this(0, 0, 0){}public SudokuCell(int x, int y, int number){X x; Y y; Number number;}public int X { get; set; }public int Y { get; set; }public int Number { get; set; }} 2.数独创建 public class …...

vscode + conda+ ffmpeg + numpy 的安装方式

Windows 搭建 环境 遇到的错误点&#xff1a; 解决&#xff0c;使用conda init conda activate myenv usage: conda-script.py [-h] [–no-plugins] [-V] COMMAND … conda-script.py: error: argument COMMAND: invalid choice: ‘activate’ (choose from ‘clean’, ‘comp…...

Python Union联合类型注解

视频版教程 Python3零基础7天入门实战视频教程 我们看下如下的示例&#xff1a; my_list2: list[int] [1, 2, 3, 4] my_dict2: dict[str, float] {"python222": 3.14, "java1234": 4.35} l1 [1, "python222", True] # 如何注解多种元素类型…...

提高接口自动化测试效率:使用 JMESPath 实现断言和数据提取!

前言 做接口自动化&#xff0c;断言是比不可少的。如何快速巧妙的提取断言数据就成了关键&#xff0c;当然也可以提高用例的编写效率。笔者在工作中接触到了JMESPath&#xff0c;那到底该如何使用呢&#xff1f;带着疑惑一起往下看。 JMESPath是啥&#xff1f; JMESPath 是一…...

【Linux操作系统教程】用户管理与权限管理你真的懂了吗(三)

&#x1f604;作者简介&#xff1a; 小曾同学.com,一个致力于测试开发的博主⛽️&#xff0c;主要职责&#xff1a;测试开发、CI/CD 如果文章知识点有错误的地方&#xff0c;还请大家指正&#xff0c;让我们一起学习&#xff0c;一起进步。&#x1f60a; 座右铭&#xff1a;不想…...

华为全联接大会2023 | 尚宇亮:携手启动O3社区发布

2023年9月20日&#xff0c;在华为全联接大会2023上&#xff0c;华为正式发布“联接全球服务工程师&#xff0c;聚合用户服务经验”的知识经验平台&#xff0c;以“Online 在线、Open 开放、Orchestration 协同”为理念&#xff0c;由华为、伙伴和客户携手&#xff0c;共同构建知…...

RocketMQ延迟消息机制

两种延迟消息 RocketMQ中提供了两种延迟消息机制 指定固定的延迟级别 通过在Message中设定一个MessageDelayLevel参数&#xff0c;对应18个预设的延迟级别指定时间点的延迟级别 通过在Message中设定一个DeliverTimeMS指定一个Long类型表示的具体时间点。到了时间点后&#xf…...

golang循环变量捕获问题​​

在 Go 语言中&#xff0c;当在循环中启动协程&#xff08;goroutine&#xff09;时&#xff0c;如果在协程闭包中直接引用循环变量&#xff0c;可能会遇到一个常见的陷阱 - ​​循环变量捕获问题​​。让我详细解释一下&#xff1a; 问题背景 看这个代码片段&#xff1a; fo…...

ESP32读取DHT11温湿度数据

芯片&#xff1a;ESP32 环境&#xff1a;Arduino 一、安装DHT11传感器库 红框的库&#xff0c;别安装错了 二、代码 注意&#xff0c;DATA口要连接在D15上 #include "DHT.h" // 包含DHT库#define DHTPIN 15 // 定义DHT11数据引脚连接到ESP32的GPIO15 #define D…...

什么是库存周转?如何用进销存系统提高库存周转率?

你可能听说过这样一句话&#xff1a; “利润不是赚出来的&#xff0c;是管出来的。” 尤其是在制造业、批发零售、电商这类“货堆成山”的行业&#xff0c;很多企业看着销售不错&#xff0c;账上却没钱、利润也不见了&#xff0c;一翻库存才发现&#xff1a; 一堆卖不动的旧货…...

linux arm系统烧录

1、打开瑞芯微程序 2、按住linux arm 的 recover按键 插入电源 3、当瑞芯微检测到有设备 4、松开recover按键 5、选择升级固件 6、点击固件选择本地刷机的linux arm 镜像 7、点击升级 &#xff08;忘了有没有这步了 估计有&#xff09; 刷机程序 和 镜像 就不提供了。要刷的时…...

【git】把本地更改提交远程新分支feature_g

创建并切换新分支 git checkout -b feature_g 添加并提交更改 git add . git commit -m “实现图片上传功能” 推送到远程 git push -u origin feature_g...

用docker来安装部署freeswitch记录

今天刚才测试一个callcenter的项目&#xff0c;所以尝试安装freeswitch 1、使用轩辕镜像 - 中国开发者首选的专业 Docker 镜像加速服务平台 编辑下面/etc/docker/daemon.json文件为 {"registry-mirrors": ["https://docker.xuanyuan.me"] }同时可以进入轩…...

大数据学习(132)-HIve数据分析

​​​​&#x1f34b;&#x1f34b;大数据学习&#x1f34b;&#x1f34b; &#x1f525;系列专栏&#xff1a; &#x1f451;哲学语录: 用力所能及&#xff0c;改变世界。 &#x1f496;如果觉得博主的文章还不错的话&#xff0c;请点赞&#x1f44d;收藏⭐️留言&#x1f4…...

C++.OpenGL (14/64)多光源(Multiple Lights)

多光源(Multiple Lights) 多光源渲染技术概览 #mermaid-svg-3L5e5gGn76TNh7Lq {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-3L5e5gGn76TNh7Lq .error-icon{fill:#552222;}#mermaid-svg-3L5e5gGn76TNh7Lq .erro…...

面向无人机海岸带生态系统监测的语义分割基准数据集

描述&#xff1a;海岸带生态系统的监测是维护生态平衡和可持续发展的重要任务。语义分割技术在遥感影像中的应用为海岸带生态系统的精准监测提供了有效手段。然而&#xff0c;目前该领域仍面临一个挑战&#xff0c;即缺乏公开的专门面向海岸带生态系统的语义分割基准数据集。受…...