彻底理解golang中什么是nil


Posted in Golang onApril 29, 2021

nil是什么

相信写过Golang的程序员对下面一段代码是非常非常熟悉的了:

if err != nil {
    // do something....
}

当出现不等于nil的时候,说明出现某些错误了,需要我们对这个错误进行一些处理,而如果等于nil说明运行正常。那什么是nil呢?查一下词典可以知道,nil的意思是无,或者是零值。零值,zero value,是不是有点熟悉?在Go语言中,如果你声明了一个变量但是没有对它进行赋值操作,那么这个变量就会有一个类型的默认零值。

这是每种类型对应的零值:

bool      -> false                              
numbers   -> 0                                
string    -> ""      
pointers -> nil
slices -> nil
maps -> nil
channels -> nil
functions -> nil
interfaces -> nil

所以,我们经常将 nil 赋值给 error 类型,并且以此来判断是否有错误,那是因为 error 是个接口,而接口的零值就是 nil

type error interface {
 Error() string
}

举个例子,当你定义了一个struct:

type Person struct {
  AgeYears int
  Name string
  Friends []Person
}
var p Person // Person{0, "", nil}

变量p只声明但没有赋值,所以p的所有字段都有对应的零值。那么,这个nil到底是什么呢?Go的文档中说到,nil是预定义的标识符,代表指针、通道、函数、接口、映射或切片的零值,也就是预定义好的一个变量:

type Type int
var nil Type

是不是有点惊讶?nil并不是Go的关键字之一,你甚至可以自己去改变nil的值:

var nil = errors.New("hi")

这样是完全可以编译得过的,但是最好不要这样子去做。

nil有什么用

在了解了什么是nil之后,再来说说nil有什么用。

pointers

var p *int
p == nil    // true
*p          // panic: invalid memory address or nil pointer dereference

指针表示指向内存的地址,如果对为nil的指针进行解引用的话就会导致panic。那么为nil的指针有什么用呢?先来看一个计算二叉树和的例子:

type tree struct {
  v int
  l *tree
  r *tree
}
// first solution
func (t *tree) Sum() int {
  sum := t.v  
  if t.l != nil {
    sum += t.l.Sum()
  }  
  if t.r != nil {
    sum += t.r.Sum()
  }    
  return sum
}

上面的代码有两个问题,一个是代码重复:

if v != nil {
  v.m()
}

另一个是当t是nil的时候会panic:

var t *tree
sum := t.Sum()   // panic: invalid memory address or nil pointer dereference

怎么解决上面的问题?我们先来看看一个指针接收器的例子:

type person struct {}
func sayHi(p *person) { fmt.Println("hi") }
func (p *person) sayHi() { fmt.Println("hi") }
var p *person
p.sayHi() // hi

对于指针对象的方法来说,就算指针的值为nil也是可以调用的,基于此,我们可以对刚刚计算二叉树和的例子进行一下改造:

func(t *tree) Sum() int {  
  if t == nil {      
    return 0
  }  
  return t.v + t.l.Sum() + t.r.Sum()
}

跟刚才的代码一对比是不是简洁了很多?对于nil指针,只需要在方法前面判断一下就ok了,无需重复判断。换成打印二叉树的值或者查找二叉树的某个值都是一样的:

func(t *tree) String() string {  
  if t == nil {    
    return ""
  }  
  return fmt.Sprint(t.l, t.v, t.r)
}
// nil receivers are useful: Find
func (t *tree) Find(v int) bool {  
  if t == nil {    
    return false
  }  
  return t.v == v || t.l.Find(v) || t.r.Find(v)
}

所以如果不是很需要的话,不要用NewX()去初始化值,而是使用它们的默认值。

slices

// nil slices
var s []slice
len(s)  // 0
cap(s)  // 0
for range s  // iterates zero times
s[i]  // panic: index out of range

一个为nil的slice,除了不能索引外,其他的操作都是可以的,当你需要填充值的时候可以使用append函数,slice会自动进行扩充。那么为nil的slice的底层结构是怎样的呢?根据官方的文档,slice有三个元素,分别是长度、容量、指向数组的指针:

彻底理解golang中什么是nil

当有元素的时候:

彻底理解golang中什么是nil

所以我们并不需要担心slice的大小,使用append的话slice会自动扩容。(视频中说slice自动扩容速度很快,不必担心性能问题,这个值得商榷,在确定slice大小的情况只进行一次内存分配总是好的)

map

对于Go来说,map,function,channel都是特殊的指针,指向各自特定的实现,这个我们暂时可以不用管。

// nil map
var m map[t]u
len(m)  // 0
for range m // iterates zero times
v, ok := m[i] // zero(u), false
m[i] = x // panic: assignment to entry in nil map

对于nil的map,我们可以简单把它看成是一个只读的map,不能进行写操作,否则就会panic。那么nil的map有什么用呢?看一下这个例子:

func NewGet(url string, headers map[string]string) (*http.Request, error) {
  req, err := http.NewRequest(http.MethodGet, url, nil)  
  if err != nil {    
    return nil, err
  }  
  for k, v := range headers {
    req.Header.Set(k, v)
  }  
  return req, nil
}

对于NewGet来说,我们需要传入一个类型为map的参数,并且这个函数只是对这个参数进行读取,我们可以传入一个非空的值:

NewGet("http://google.com", map[string]string{  
"USER_AGENT": "golang/gopher",
},)

或者这样传:

NewGet("http://google.com", map[string]string{})

但是前面也说了,map的零值是nil,所以当header为空的时候,我们也可以直接传入一个nil:

NewGet("http://google.com", nil)

是不是简洁很多?所以,把nil map作为一个只读的空的map进行读取吧。

channel

// nil channels
var c chan t
<- c      // blocks forever
c <- x    // blocks forever
close(c)  // panic: close of nil channel

关闭一个nil的channel会导致程序panic(如何关闭channel可以看这篇文章:如何优雅地关闭Go channel)举个例子,假如现在有两个channel负责输入,一个channel负责汇总,简单的实现代码:

func merge(out chan<- int, a, b <-chan int) {  
  for {    
    select {      
      case v := <-a:
        out <- v      
      case v := <- b:
        out <- v
    }
  }
}

如果在外部调用中关闭了a或者b,那么就会不断地从a或者b中读出0,这和我们想要的不一样,我们想关闭a和b后就停止汇总了,修改一下代码:

func merge(out chan<- int, a, b <-chan int) {  
  for a != nil || b != nil {    
    select {      
      case v, ok := <-a:          
         if !ok {
            a = nil
            fmt.Println("a is nil")            
            continue
          }
          out <- v      
      case v, ok := <-b:          
          if !ok {
            b = nil
            fmt.Println("b is nil")            
            continue
          }
          out <- v
    }
  }
  fmt.Println("close out")  
  close(out)
}

在知道channel关闭后,将channel的值设为nil,这样子就相当于将这个select case子句停用了,因为nil的channel是永远阻塞的。

interface

interface并不是一个指针,它的底层实现由两部分组成,一个是类型,一个值,也就是类似于:(Type, Value)。只有当类型和值都是nil的时候,才等于nil。看看下面的代码:

func do() error {   // error(*doError, nil)
  var err *doError  
  return err  // nil of type *doError
}
func main() {
  err := do()
  fmt.Println(err == nil)
}

输出结果是false。do函数声明了一个*doErro的变量err,然后返回,返回值是error接口,但是这个时候的Type已经变成了:(*doError,nil),所以和nil肯定是不会相等的。所以我们在写函数的时候,不要声明具体的error变量,而是应该直接返回nil:

func do() error {  
  return nil
}

再来看看这个例子:

func do() *doError {  // nil of type *doError
  return nil
}
func wrapDo() error { // error (*doError, nil)
  return do()       // nil of type *doError
}
func main() {
  err := wrapDo()   // error  (*doError, nil)
  fmt.Println(err == nil) // false
}

这里最终的输出结果也是false。为什么呢?尽管wrapDo函数返回的是error类型,但是do返回的却是*doError类型,也就是变成了(*doError,nil),自然也就和nil不相等了。因此,不要返回具体的错误类型。遵从这两条建议,才可以放心地使用if x != nil。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持三水点靠木。如有错误或未考虑完全的地方,望不吝赐教。

Golang 相关文章推荐
Go语言操作数据库及其常规操作的示例代码
Apr 21 Golang
golang 实现对Map进行键值自定义排序
Apr 28 Golang
golang 实现两个结构体复制字段
Apr 28 Golang
基于Go Int转string几种方式性能测试
Apr 28 Golang
Go 自定义package包设置与导入操作
May 06 Golang
Go 语言下基于Redis分布式锁的实现方式
Jun 28 Golang
详解Go语言Slice作为函数参数的使用
Jul 02 Golang
浅谈GO中的Channel以及死锁的造成
Mar 18 Golang
实现GO语言对数组切片去重
Apr 20 Golang
Golang bufio详细讲解
Apr 21 Golang
Golang解析JSON对象
Apr 30 Golang
Go语言入门exec的基本使用
May 20 Golang
基于Go Int转string几种方式性能测试
Apr 28 #Golang
Go语言中break label与goto label的区别
golang 如何用反射reflect操作结构体
Apr 28 #Golang
golang 生成对应的数据表struct定义操作
Apr 28 #Golang
golang 如何通过反射创建新对象
Apr 28 #Golang
golang 实现两个结构体复制字段
Apr 28 #Golang
go结构体嵌套的切片数组操作
Apr 28 #Golang
You might like
一个域名查询的程序
2006/10/09 PHP
php网站判断用户是否是手机访问的方法
2013/11/01 PHP
PHP安全的URL字符串base64编码和解码
2014/06/19 PHP
php数组操作之键名比较与差集、交集赋值的方法
2014/11/10 PHP
简要剖析PHP的Yii框架的组件化机制的基本知识
2016/03/17 PHP
PHP getallheaders无法获取自定义头(headers)的问题
2016/03/23 PHP
Yii2.0中的COOKIE和SESSION用法
2016/08/12 PHP
thinkPHP实现多字段模糊匹配查询的方法
2016/12/01 PHP
基于ThinkPHP实现的日历功能实例详解
2017/04/15 PHP
javascript结合html5 canvas实现(可调画笔颜色/粗细/橡皮)的涂鸦板
2013/04/27 Javascript
jQuery实现当按下回车键时绑定点击事件
2014/01/28 Javascript
jQuery实现的背景动态变化导航菜单效果
2015/08/24 Javascript
jquery 仿锚点跳转到页面指定位置的实例
2017/02/14 Javascript
Angular+Node生成随机数的方法
2017/06/16 Javascript
js中apply与call简单用法详解
2017/11/06 Javascript
nodejs async异步常用函数总结(推荐)
2017/11/17 NodeJs
ng-repeat指令在迭代对象时的去重方法
2018/10/02 Javascript
Vue一次性简洁明了引入所有公共组件的方法
2018/11/28 Javascript
JS实现点击li标签弹出对应的索引功能【案例】
2019/02/18 Javascript
javascript严格模式详解(含严格模式与非严格模式的区别)
2019/11/12 Javascript
Python中的复制操作及copy模块中的浅拷贝与深拷贝方法
2016/07/02 Python
Python金融数据可视化汇总
2017/11/17 Python
Python网络编程基于多线程实现多用户全双工聊天功能示例
2018/04/10 Python
pandas.DataFrame选取/排除特定行的方法
2018/07/03 Python
Python获取时间戳代码实例
2019/09/24 Python
TensorFlow设置日志级别的几种方式小结
2020/02/04 Python
python使用html2text库实现从HTML转markdown的方法详解
2020/02/21 Python
Python命名空间及作用域原理实例解析
2020/08/12 Python
7款设计巧妙的css3飘带状3D立体效果的导航菜单和表单窗口
2013/02/04 HTML / CSS
当当网软件测试笔试题
2015/11/24 面试题
高校毕业生登记表自我鉴定
2013/11/03 职场文书
计算机个人求职信范例
2014/01/24 职场文书
总经理工作职责范文
2014/03/14 职场文书
《时代广场的蟋蟀》读后感:真挚友情,温暖世界!
2020/01/08 职场文书
Redis实现订单自动过期功能的示例代码
2021/05/08 Redis
Python自动化测试PO模型封装过程详解
2021/06/22 Python