Go疑难杂症讲解之为什么nil不等于nil
来源:脚本之家    时间:2022-10-28 06:10:30
目录
现象寻找原因总结

现象

在日常开发中,可能一不小心就会掉进 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)) //  
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!")
}

输出如下:


*err.CustomizedError

在一开始,我们给 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对应的类型信息就是 erroritab._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

总结

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

以上就是Go疑难杂症讲解之为什么nil不等于nil的详细内容,更多关于Go nil不等于nil的资料请关注脚本之家其它相关文章!

关键词: 表示的是 类型变量 变量赋值 疑难杂症

上一篇:

下一篇:

X 关闭

X 关闭