深度剖析 Go 的 nil

发表于 3年以前  | 总阅读数:228 次

坚持思考,就会很酷

前几天有小伙伴问我说,golang 里面很多类型使用 nil 来赋值和做条件判断,总是混淆记不住。你可能见过:

  1. 很多文章和书会教你:Go 语言默认定义的类型赋值会被 nil
  2. error 返回值经常用 return nil 的写法;
  3. 多种类型都可以使用 if 是否 != nil

上面的事情在 Go 编程里随处可见,下面思考几个问题,看自己对 nil 这个知识点是否做到了知其所以然

  1. nil 是一个关键字?还是类型?还是变量?
  2. 并非所有类型都跟 nil 有关系,有哪些类型可以使用 != nil 的语法?
  3. 这些不同的类型和 nil 打交道又有什么异同?
  4. 为什么有些复合结构定义了变量还不够,还必须要 make(Type) 才能使用 ?否则会出 panic
  5. 很多书里讲 slice 也要 make 之后才能用,但其实不必要,其实 slice 只要定义了就能用。map 结构却光定义还不行,一定要 make(Type) 才能使用

下面我们就这几个思考题展开,剖析 nil 的秘密。

Go 里面 nil 到底是什么?

我们思考的第一个问题是:nil 是一个关键字?还是类型?还是变量?

答案自然是:变量。具体是什么样的变量,我们可以点进去 Go 的源码看下:

一窥 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

从类型定义得到两个关键点

  1. nil 本质上是一个 Type 类型的变量而已;
  2. Type 类型仅仅是基于 int 定义出来的一个新类型;

nil 官方的注释中,我们可以得到一个重要信息:

划重点nil 适用于 指针函数interfacemapslicechannel 这 6 种类型。

Go 和 C 的变量定义异同

相同点

Go 和 C 的变量定义回归最本质原理:分配变量指定大小的内存,确定一个变量名称。

不同点

  • Go 分配内存是置 0 分配的。置 0 分配的意思是:Go 确保分配出来的内存块里面是全 0 数据;
  • C 默认分配的内存则仅仅是分配内存,里面的数据不能做任何假设,里面是未定义的数据,可能是全 0 ,可能是全 1,可能是 0101 等;

Go 置 0 分配的原理

  • 栈上变量的内存编译阶段由编译器就保证了置 0 分配,这种反汇编看下就知道了;
  • 堆上变量的内存由 runtime 保证,可以仔细观察下 mallocgc 这个函数参数有一个 needzero 的参数,用户变量定义触发的入口(比如 newobject 等等 )这个参数为 true,而该参数就是显式指定置 0 分配的。
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
    // ...
}

思考一个小问题:Go 既然所用的类型定义都是置 0 分配的,那为什么 mallocgc 需要 needzero 这么一个参数来控制呢?

首先,Go 的类型定义一定确保是置 0 分配的,这个是 Go 语言给到 Go 程序员的语义。Go runtime 众多的内部的流程(对 Go 程序员不感知的层面)是没有这个规定的。其次,置 0 分配是有性能代价的,如果在确保语义的情况下,能不做自然是最好的。

划重点:Go 的变量定义由语言层面确保置 0 分配,确保内存块全 0 数据。请记住这个最本质的约定。

怎么理解 nil

通过上面,我们理解了几个东西:

  1. Go 的类型定义仅比 C 多做了一件事,把分配的内存块置 0,而已;
  2. 能够和 nil 值做判断的,仅仅有 6 个类型。如果你用来其他类型来和 nil 比较,那么在编译期间 typecheck 会报错检查到会报错;

就笔者理解,nil 这个概念是更高一层的概念,在语言级别,而这个概念是由编译器带给你的。不是所有的类型都可以和 nil 进行比较或者赋值,只有这 6 种类型的变量才能和 nil 值比较,因为这是编译器决定的。

同样的,你不能赋值一个 nil 变量给一个整型,原理也很简单,仅仅是编译器不让,就这么简单。

所以,nil 其实更准确的理解是一个触发条件,编译器看到和 nil 值比较的写法,那么就要确认类型在这 6 种类型以内,如果是赋值 nil,那么也要确认在这 6 种类型以内,并且对应的结构内存为全 0 数据。

所以,记住这句话,**nil 是编译器识别行为的一个触发点而已,看到这个 nil 会触发编译器的一些特殊判断和操作。**

和 nil 打交道的 6 大类型

slice 类型

变量定义

创建 slice 的本质上是 2 种:

  1. var 关键字定义;
  2. make 关键字创建;
// 方式一
var slice1 []byte
var slice2 []byte = []byte{0x1, 0x2, 0x3}

// 方式二
var slice3 = make([]byte, 0)
var slice4 = make([]byte, 3)

首先,slice 变量本身占多少个字节?

答案是:24 个字节。1 个指针字段,2 个 8 字节的整形字段。

思考:varmake 这两种方式有什么区别?

  • 第一种 var 的方式定义变量纯粹真的是变量定义,如果逃逸分析之后,确认可以分配在栈上,那就在栈上分配这 24 个字节,如果逃逸到堆上去,那么调用 newobject 函数进行类型分配。
  • 第二种 make 方式则略有不同,如果逃逸分析之后,确认分配在栈上,那么也是直接在栈上分配 24 字节,如果逃逸到堆上则会导致调用 makeslice 函数来分配变量。

变量本身

定义的变量本身分配了多少内存?

上面已经说过了,无论多大的 slice ,变量本身占用 24 字节。这 24 个字节其实是动态数组的管理结构,如下:

type slice struct {
   array unsafe.Pointer         // 管理的内存块首地址
   len   int                    // 动态数组实际使用大小
   cap   int                    // 动态数组内存大小
}

该结构体定义在 src/runtime/slice.go 里。

划重点:我们看到无论是 var 声明定义的 slice 变量,还是 make(xxx,num) 创建的 slice 变量,slice 管理结构是已经分配出来了的(也就是 struct slice 结构 )。

所以, 对于 slice 来说,其实并不需要 make 创建的才能使用,直接用 var 定义出来的 slice 也能直接使用。如下:

// 定义一个 slice
var slice1 []byte
// 使用这个 slice
slice1 = append(slice1, 0x1)

定义的时候,slice 结构本身就已经置 0 分配了,这个 24 字节的 slice 结构就是管理动态数组的核心。有这个在 append 函数就能正常处理 slice 变量。

思考:append 又是怎么处理的呢?

本质是调用 runtime.growslice 函数来处理。

<span style="font-size: 14px;color: rgb(2, 30, 170);">nil 赋值

如果把一个已经存在的 slice 结构赋值 nil ,会发生什么事情?

var slice2 []byte = []byte{0x1, 0x2, 0x3}

// slice 赋值 nil
slice2 = nil

发生什么事?

事情在编译期间就确定了,就是把 slice2 变量本身内存块置 0 ,也就是说 slice2 本身的 24 字节的内存块被置 0。

nil 值判断

编译器认为 slice 做可以做 nil 判断,那么什么样的 slice 认为是 nil 的?

指针值为 0 的,也就是说这个动态数组没有实际数据的时候。

思考:仅判断指针?对 len 和 cap 两个字段不做判断吗?

只对首字段 array 做非 0 判断,len,cap 字段不做判断。

如下:

var a []byte = []byte{0x1, 0x2, 0x3}
if a != nil {
}

对应的部分汇编代码如下:

// 赋值 array 的值
0x00000000004587cd <+93>: mov    %rax,0x20(%rsp)
// 赋值 len 的值
0x00000000004587d2 <+98>: movq   $0x3,0x28(%rsp)
// 赋值 cap 的值
0x00000000004587db <+107>: movq   $0x3,0x30(%rsp)
// 判断 slice 是否是 nil
=> 0x00000000004587e4 <+116>: test   %rax,%rax

不信 Go 只判断首字段?为了验证,自己思考下一下的程序的输出:

package main

import (
   "unsafe"
)

type sliceType struct {
   pdata unsafe.Pointer
   len   int
   cap   int
}

func main() {
   var a []byte

   ((*sliceType)(unsafe.Pointer(&a))).len = 0x3
   ((*sliceType)(unsafe.Pointer(&a))).cap = 0x4

   if a != nil {
      println("not nil")
   } else {
      println("nil")
   }
}

答案是:输出 nil

map 类型

变量定义

// 变量定义
var m1 map[string]int
// 定义 & 初始化
var m2 = make(map[string]int)

和 slice 类似,上面也是两种差别的方式:

  • 第一种方式仅仅定义了 m1 变量本身;
  • 第二种方式则是分配 m2 的内存,还会调用 makehmap 函数(不一定是这个函数,要看逃逸分析的结果,如果是可以栈上分配的,会有一些优化)来创建某个结构,并且把这个函数的返回值赋给 m2;

变量本身

map 的变量本身究竟是什么?比如上面的 m1m2 ?

m1, m2 变量本身是一个指针,内存占用 8 字节。这个指针指向的结构才大有来头,指向一个 struct hmap 结构。

type hmap struct {
   count     int // # live cells == size of map.  Must be first (used by len() builtin)
   flags     uint8
   B         uint8  // log_2 of # of buckets (can hold up to loadFactor * 2^B items)
   noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details
   hash0     uint32 // hash seed

   buckets    unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.
   oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing
   nevacuate  uintptr        // progress counter for evacuation (buckets less than this have been evacuated)

   extra *mapextra // optional fields
}

所以,回到思考问题:为什么 map 结构却光定义还不行,一定要 make(XXMap) 才能使用?

因为,map 结构的核心在于 struct hmap 结构体,这个结构体是很大的一个结构体。map 的操作核心都是基于这个结构体之上的。而 var 定义一个 map 结构的时候,只是分配了一个 8 字节的指针,只有调用 make 的时候,才触发调用 makemap ,在这个函数里面分配出一个庞大的 struct hmap 结构体。

nil 赋值

如果把一个 map 变量赋值 nil 那就很容易理解了,仅仅是把这个变量本身置 0 而已,也就是这个指针变量置 0 ,hmap 结构体本身是不会动的。

当然考虑垃圾回收的话,如果这个 m1 是唯一的指向这个 hmap 结构,那么 m1 赋值 nil 之后,那么这个 hmap 结构体之后就可能被回收。

nil 值判断

搞懂了变量本身和管理结构的区别就很简单了,这里的 nil 值判断也仅仅是针对变量本身的判断,只要是非 0 指针,那么就是非 nil 。也就是说 m1 只要是一个非 0 的指针,就不会是非nil 的。

package main

func main() {
   var m1 map[string]int
   var m2 = make(map[string]int)
   if m1 != nil {
      println("m1 not nil")
   } else {
      println("m1 nil")
   }
   if m2 != nil {
      println("m2 not nil")
   } else {
      println("m2 nil")
   }
}

如上示例程序,m1 是一个 0 指针,m2 被赋值了的。

interface 类型

变量定义

// 定义一个接口
type Reader interface {
   Read(p []byte) (n int, err error)
}

// 定义一个接口变量
var reader Reader
// 或者一个空接口
var empty interface{}

变量本身

interface 稍微有点特殊,有两种对应的结构体,如下:

type iface struct {
    tab  *itab
    data unsafe.Pointer
}

type eface struct {
    _type *_type
    data  unsafe.Pointer
}

其中,iface 就是通常定义的 interface 类型,eface 则是通常人们常说的空接口 对应的数据结构。

不管内部怎么样,这两个结构体占用内存是一样的,都是一个正常的指针类型和一个无类型的指针类型( Pointer ),总共占用 16 个字节。

也就是说,如果你声明定义一个 interface 类型,无论是空接口,还是具体的接口类型,都只是分配了一个 16 字节的内存块给你,注意是置 0 分配哦。

nil 赋值

和上面类似,如果对一个 interface 变量赋值 nil 的话,发生的事情也仅仅是把变量本身这 16 个字节的内存块置 0 而已。

nil 值判断

判断 interface 是否是 nil ?这个跟 slice 类似,也仅仅是判断首字段(指针类型)是否为 0 即可。因为如果是初始化过的,首字段一定是非 0 的。

channel 类型

变量定义

// 变量本身定义
var c1 chan struct{}
// 变量定义和初始化
var c2 = make(chan struct{})

区别:

  • 第一种方式仅仅定义了 c1 变量本身;
  • 第二种方式则是分配 c2 的内存,还会调用 makechan 函数来创建某个结构,并且把这个函数的返回值赋给 c2;

变量本身

定义的 channel 变量本身是什么一个表现?

答案是:一个 8 字节的指针而已,意图指向一个 channel 管理结构,也就是 struct hchan 的指针。

程序员定义的 channel 变量本身内存仅仅是一个指针,channel 所有的逻辑都在 hchan 这个管理结构体上,所以,channel 也是必须 make(chan Xtype) 之后才能使用,就是这个道理。

nil 赋值

赋值 nil 之后,仅仅是把这 8 字节的指针置 0 。

nil 值判断

简单,仅仅是判断这 channel 指针是否非 0 而已。

指针 类型

指针和函数类型比较好理解,因为之前的 4 种类型 slicemapchannelinterface 是复合结构。

指针本身来说也只是一个 8 字节的整型,函数变量类型则本身就是个指针。

变量定义

var ptr *int

变量本身

变量本身就是一个 8 字节的内存块,这个没啥好讲的,因为指针都不是复合类型。

nil 赋值

ptr = nil

这 8 字节的指针置 0。

nil 值判断

判断这 8 字节的指针是否为 0 。

函数 类型

变量定义

var f func(int) error

变量本身

变量本身是一个 8 字节的指针。

nil 赋值

本身就是指针,只不过指向的是函数而已。所以赋值也仅仅是这 8 字节置 0 。

nil 值判断

判断这 8 字节是否为 0 。

总结

下面总结一些上述分享:

  1. 请撇开死记硬背的语法和玄学,变量仅仅是绑定到一个指定内存块的名字;
  2. Go 从语言层面对程序员做了承诺,变量定义分配的内存一定是置 0 分配的;
  3. 并不是所有的类型能够赋值 nil,并且和 nil 进行对比判断。只有 slicemapchannelinterface、指针、函数 这 6 种类型;
  4. 不要把 nil 理解成一个特殊的值,而要理解成一个触发条件,编译器识别到代码里有 nil 之后,会对应做出处理和判断;
  5. channelmap 类型的变量必须要 make 才能使用的原因(否则会出现空指针的 panic )在于 var 定义的变量仅仅是分配了一个指向 hchanhmap 的指针变量而已,并且还是置 0 分配的。真正的管理结构只有 make 调用才能分配出来,对应的函数分别是 makechanmakemap 等;
  6. slice 变量为什么 var 就能用是因为 struct slice 核心结构是定义的时候就分配出来了
  7. 以上 6 种变量赋值 nil 的行为都是把变量本身置 0 ,仅此而已。slice 的 24 字节管理结构,map 的 8 字节指针,channel 的 8 字节指针,interface 的 16 字节,8 字节指针和函数指针也是如此;
  8. 以上 6 种类型和 nil 进行比较判断本质上都是和变量本身做判断,slice 是判断管理结构的第一个指针字段mapchannel 本身就是指针,interface 也是判断管理结构的第一个指针字段,指针和函数变量本身就是指针;

后记

推荐使用 gdb 进行对上面的 demo 程序进行调试,加深自己理解。重点关注内存分配和内部代码的生成(反汇编),比如类似 makechan 这样的函数,如果你不调试,你根本不会知道竟然还有这个,我明明没有写过这函数呀?这个是编译器帮你生成的

~完~

本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/sHLYy_4XA6254-vLmlu0IA

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237231次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8065次阅读
 目录