• [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)) //  
    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 
    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> 
    *err.CustomizedError <nil>

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

    txn, err := startTx()
    fmt.Println(reflect.TypeOf(err), reflect.ValueOf(err)) //  
    
    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 

    所以在做 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

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


    __EOF__

  • 本文作者: 弗兰克的猫
  • 本文链接: https://www.cnblogs.com/mfrank/p/16831877.html
  • 关于博主: 终生学习,终生成长
  • 版权声明: 版权归博主所有,转载请注明出处
  • 声援博主: 如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。
  • 相关阅读:
    [自制操作系统] 第19回 实现用户进程(下)
    Smart UI Web 16.0.1 WebComponents htmlelements Crack
    PNAS:慢波振荡促进长程有效沟通:故障网络中记忆巩固的关键
    搭建成功simulink-stm32硬件在环开发环境
    P1387 最大正方形-前缀和
    [NLP]LLM--使用LLama2进行离线推理
    安全加速SCDN在网站运营中的重要作用
    大结局:奥特曼等人将加入微软
    用HTML、CSS和JS打造绚丽的雪花飘落效果
    Mac本地安装PHP@7.4版本 - 2024
  • 原文地址:https://www.cnblogs.com/mfrank/p/16831877.html