当前位置: 首页 > 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;共同构建知…...

C++_核心编程_多态案例二-制作饮品

#include <iostream> #include <string> using namespace std;/*制作饮品的大致流程为&#xff1a;煮水 - 冲泡 - 倒入杯中 - 加入辅料 利用多态技术实现本案例&#xff0c;提供抽象制作饮品基类&#xff0c;提供子类制作咖啡和茶叶*//*基类*/ class AbstractDr…...

RocketMQ延迟消息机制

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

Redis相关知识总结(缓存雪崩,缓存穿透,缓存击穿,Redis实现分布式锁,如何保持数据库和缓存一致)

文章目录 1.什么是Redis&#xff1f;2.为什么要使用redis作为mysql的缓存&#xff1f;3.什么是缓存雪崩、缓存穿透、缓存击穿&#xff1f;3.1缓存雪崩3.1.1 大量缓存同时过期3.1.2 Redis宕机 3.2 缓存击穿3.3 缓存穿透3.4 总结 4. 数据库和缓存如何保持一致性5. Redis实现分布式…...

Leetcode 3577. Count the Number of Computer Unlocking Permutations

Leetcode 3577. Count the Number of Computer Unlocking Permutations 1. 解题思路2. 代码实现 题目链接&#xff1a;3577. Count the Number of Computer Unlocking Permutations 1. 解题思路 这一题其实就是一个脑筋急转弯&#xff0c;要想要能够将所有的电脑解锁&#x…...

MMaDA: Multimodal Large Diffusion Language Models

CODE &#xff1a; https://github.com/Gen-Verse/MMaDA Abstract 我们介绍了一种新型的多模态扩散基础模型MMaDA&#xff0c;它被设计用于在文本推理、多模态理解和文本到图像生成等不同领域实现卓越的性能。该方法的特点是三个关键创新:(i) MMaDA采用统一的扩散架构&#xf…...

学习STC51单片机31(芯片为STC89C52RCRC)OLED显示屏1

每日一言 生活的美好&#xff0c;总是藏在那些你咬牙坚持的日子里。 硬件&#xff1a;OLED 以后要用到OLED的时候找到这个文件 OLED的设备地址 SSD1306"SSD" 是品牌缩写&#xff0c;"1306" 是产品编号。 驱动 OLED 屏幕的 IIC 总线数据传输格式 示意图 …...

【JavaWeb】Docker项目部署

引言 之前学习了Linux操作系统的常见命令&#xff0c;在Linux上安装软件&#xff0c;以及如何在Linux上部署一个单体项目&#xff0c;大多数同学都会有相同的感受&#xff0c;那就是麻烦。 核心体现在三点&#xff1a; 命令太多了&#xff0c;记不住 软件安装包名字复杂&…...

有限自动机到正规文法转换器v1.0

1 项目简介 这是一个功能强大的有限自动机&#xff08;Finite Automaton, FA&#xff09;到正规文法&#xff08;Regular Grammar&#xff09;转换器&#xff0c;它配备了一个直观且完整的图形用户界面&#xff0c;使用户能够轻松地进行操作和观察。该程序基于编译原理中的经典…...

基于matlab策略迭代和值迭代法的动态规划

经典的基于策略迭代和值迭代法的动态规划matlab代码&#xff0c;实现机器人的最优运输 Dynamic-Programming-master/Environment.pdf , 104724 Dynamic-Programming-master/README.md , 506 Dynamic-Programming-master/generalizedPolicyIteration.m , 1970 Dynamic-Programm…...

return this;返回的是谁

一个审批系统的示例来演示责任链模式的实现。假设公司需要处理不同金额的采购申请&#xff0c;不同级别的经理有不同的审批权限&#xff1a; // 抽象处理者&#xff1a;审批者 abstract class Approver {protected Approver successor; // 下一个处理者// 设置下一个处理者pub…...