前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >理解Golang的nil

理解Golang的nil

原创
作者头像
chandlerpan
发布2022-07-12 12:25:05
5560
发布2022-07-12 12:25:05
举报
文章被收录于专栏:程序员的自我修养

令人迷惑的nil

代码语言:go
复制
type Message struct {
	A *Message
}

func (x *Message) GetA() *Message {
	if x != nil {
		return x.A
	}
	return nil
}
func TestNil(t *testing.T) {
	var s *Message
	var v interface{} = s

	fmt.Println(v == s)                        // #=> true
	fmt.Println(s == nil)                      // #=> true
	fmt.Println(v == nil)                      // #=> false
	fmt.Println(s.GetA().GetA().GetA() == nil) // #=> true
}
  • 问题一:snil,为什么赋值给v就不是nil了?
  • 问题二:snilv不是nil,为什么s还等于v
  • 问题三:s.GetA()返回的是nil,为什么nil还能继续调用GetA()方法?

下面是我们常见的一种golang错误处理的坑,即自定义错误对象:

代码语言:go
复制
type Err struct {
	err string
}

func (e *Err) Error() string {
	return e.err
}
func returnErr() *Err {
	return nil
}
func TestErr(t *testing.T) {
	var err error
	err = returnErr()
	fmt.Println(err, err != nil) // #=> true
}

这里和上面是相同的问题,即返回的nil为什么不等于nil?

nil

nil在golang中是一个预设值:

代码语言:go
复制
// nil is a predeclared identifier representing the zero value for a
// pointer, channel, func, interface, map, or slice type.
var nil Type // Type must be a pointer, channel, func, interface, map, or slice type

// Type is here for the purposes of documentation only. It is a stand-in
// for any Go type, but represents the same type for any given function
// invocation.
type Type int

可以看到,nil初始是0。

interface

golang的interface是一种内置类型,严格来讲它算是goalng提供的一种语法糖,辅助编码用的,它在运行时会转换成两种类型(位于包/usr/local/go/src/runtime/runtime2.go中):

代码语言:go
复制
type iface struct {
	tab  *itab
	data unsafe.Pointer
}
type eface struct {
	_type *_type
	data  unsafe.Pointer
}
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.
}

iface是指包含方法的接口,eface是指不含方法的接口,特指interface{},goalng将其独立出来节省部分存储空间。

我们这里专注讨论interface{},我们在编码中可以将任意类型转成interface{},但是interface{}并不是任意类型,它在被赋值后就是一种特有类型。其中_type起到至关重要的作用,它是 Go 语言类型的运行时表示。下面是运行时包中的结构体,其中包含了很多类型的元信息,例如:类型的大小、哈希、对齐以及种类等。

代码语言:go
复制
type _type struct {
	size       uintptr
	ptrdata    uintptr // size of memory prefix holding all pointers
	hash       uint32
	tflag      tflag
	align      uint8
	fieldAlign uint8
	kind       uint8
	// function for comparing objects of this type
	// (ptr to object A, ptr to object B) -> ==?
	equal func(unsafe.Pointer, unsafe.Pointer) bool
	// gcdata stores the GC type data for the garbage collector.
	// If the KindGCProg bit is set in kind, gcdata is a GC program.
	// Otherwise it is a ptrmask bitmap. See mbitmap.go for details.
	gcdata    *byte
	str       nameOff
	ptrToThis typeOff
}

size 字段存储了类型占用的内存空间,为内存空间的分配提供信息;

hash 字段能够帮助我们快速确定类型是否相等;

equal 字段用于判断当前类型的多个对象是否相等;

编译过程

我们将上面的示例编译成汇编语言,查看汇编代码:

代码语言:shell
复制
go tool compile -S -N -l nil_test.go
代码语言:text
复制
➜  nil git:(master) ✗ go tool compile -S -N -l nil_test.go                                            .
"".TestNil STEXT size=634 args=0x8 locals=0xe8 funcid=0x0 align=0x0
TEXT    "".TestNil(SB), ABIInter # func TestNil(t *testing.T) {
MOVQ    $0, "".s+32(SP)          # var s *Message : 将0赋值给[s栈32位置]
LEAQ    type.*"".Message(SB), DX # var v interface{} = s : 将Message类型有效地址赋值给DX寄存器
MOVQ    DX, "".v+96(SP)          # var v interface{} = s : 将DX寄存器内容(Message类型地址)赋值给[v栈96位置]
MOVQ    $0, "".v+104(SP)         # var v interface{} = s : 将0赋值给[v栈的104位置](给v赋值)
MOVQ    "".s+32(SP), SI          # fmt.Println(v == s) : 将[s栈32位置]赋值给SI寄存器(值为0)
CMPQ    "".v+104(SP), SI         # fmt.Println(v == s) : 对比[v栈104位置]和[s栈32位置值](都是0)
SETEQ   DL                       # fmt.Println(v == s) : 将对比值赋值给DL寄存器
CALL    fmt.Println(SB)          # fmt.Println(v == s) : 打印对比结果,true
CMPQ    "".s+32(SP), $0          # fmt.Println(s == nil) : 对比[s栈32位置]和0值(都是0)
CALL    fmt.Println(SB)          # fmt.Println(s == nil) : 打印对比结果,true
CMPQ    "".v+96(SP), $0          # fmt.Println(v == nil) : 对比[v栈96位置]和0值([v栈96位置]是有值的,因此结果是false)
CALL    fmt.Println(SB)          # fmt.Println(v == nil) : 打印对比结果,false
MOVQ    "".s+32(SP), AX          # fmt.Println(s.GetA().GetA().GetA() == nil) :
CALL    "".(*Message).GetA(SB)   # fmt.Println(s.GetA().GetA().GetA() == nil) : 汇编会将nil转成*Message类型

结论

结合interface{}定义和汇编结果我们可以发现:

  • s的整体堆栈是0,它和nil对比输出是true
  • s确实是nil,但是v是有值的,它拿到了一个Message类型的地址v栈96位置
  • s和v作比较对比的是它们的值(应该是成员变量A的值),所以二者是相等的
  • v和nil作比较对比的是v的整个栈内容是否为0,所以输出false
  • s调用Get方法会编译成Message指针调用Get方法,所以不会报错

从编码角度看:

  • s是*nil.Message类型,其成员变量A值为0
  • v在运行时是eface类型,其中成员变量data unsafe.Pointer指向s的地址,成员变量_type *_type不为空,size,hash,equal都是有值的,所以v不是nil。
  • ==执行的是equal函数,判断s和v两者的值是否相等,其内部成员变量A都是默认零值,所以相等。

下面是堆栈的图示:

堆栈高度

0-32

32-56

56-104

104-112

112-120

堆栈内容

函数

s的值(0)

0

v的_type(Message类型指针)

v的值(0)

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 令人迷惑的nil
  • nil
  • interface
  • 编译过程
  • 结论
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档