Golang之nil的妙用

前言

在2016年的GopherCon上,Francesc Campoy分享了《Understanding nil》,视频连接:https://www.youtube.com/watch?v=ynoY2xz-F8s&t=587s .网上也有大神对这个视频的内容作了总结,如:https://www.jianshu.com/p/dd80f6be7969 以及 https://blog.keyboardman.me/2018/05/12/nil-in-go/ 。我无法验证他们是否原创,但是其他转发的文章都将原文指向这两篇文章。如果我搞错了,欢迎指正。

本文尝试对视频分享的内容以自己的理解作一次笔记,方便自己加深记忆。如果还能够帮到大家一点点,那是再好不过了。由于英文水平有限,如果文中有错,同样欢迎指正。

nil是什么

nil和null类似,都是表示空/零(zero)或者元(中华文字博大精深,Alpha zero被国人成为阿尔法元),如元年元月元日,即最初始的那个值。下图截自该视频,数据来源是维基百科。图中可以看到,nil和null一样是数值0的另一个称呼。
Golang之nil的妙用_第1张图片

Go语言中的nil是什么

在Go语言中,布尔类型的"0"(初始值)为false,数值类型的"0"为0,字符串类型的"0"为空字符串"",而指针/切片/映射/通道/函数和接口的"0"即为nil。
Golang之nil的妙用_第2张图片
当你声明一个结构体变量并未初始化时,该结构体所有的域(Field)都为"0"(初始值):

type Person struct {
	AgeYears int
	Name     string
	Friend   []Person
}

var p Person	//Person{0,"",nil}

nil不是关键字(keyword)

这里需要注意的是nil不是Go语言的关键字,即下面的语句可以通过编译,但是你不应该这么做:

var nil = errors.New("my god")

nil的意义是什么

  • 当变量的类型为指针时,nil表示该指针不指向任何值,即不能从指针中取值。
  • 当变量的类型为切片时,nil表示该切片没有backing array(不知道怎么翻译好),即切片的长度和容量都为0,系统没有为该切片分配存储空间。
  • 当变量的类型为映射/通道/函数时,nil表示该变量未初始化。未初始化的映射只能读不能写。未初始化的通道只能读不能写,且读会造正阻塞。未初始化的函数不能被调用。
  • 当变量的类型为接口时,nil表示该变量没有值,也不能是一个空指针。接口由两部分组成,一部分是类型,另一部分是值,只有当类型和值都为nil时,该变量才等于nil。
    Golang之nil的妙用_第3张图片

nil有什么用

这一部分是该视频的重点,大概占了视频一半的时间,分别从各种变量类型讲解了nil使用上的优势和便捷。

nil指针

和其他语言稍微不同的是,Go语言的函数接收器(receiver)允许nil的存在,即下面的代码可以编译通过:

func (p *Person) SayHi(){
	fmt.Println("Hi")
}

var p *Person
p.SayHi()	// print "Hi"

这个特性让我们无需在每次调用方法前判断指针是否为nil,如:

type node struct{
	value int
	next *node
}
func (n *node) Sum() int{
    s:=0
	if n.next != nil{
		s = n.next.Sum()
	}
	return n.value + s
}
var n *node
if n != nil {
	n.Sum()
}

而是在方法内判断receiver是否nil,可以简省不少代码:

type node struct{
	value int
	next *node
}
func (n *node) Sum() int{
	if n == nil{
		return 0
	}
	return n.value + n.next.Sum()
}
var n *node
n.Sum()

nil切片

nil切片是长度和容量都为0的切片,在使用中如果没有必要,我们完全可以不初始化nil切片,因为nil切片也有切片的功能,如:

var ss []string	// nil切片
len(ss) // 0
cap(ss) // 0
for s := range ss // 迭代0次
ss[i] // panic:index out of range

nil切片还可以直接append数据,如:

var ss []string
ss = append(ss,"hello world") // ss ["hello world"]

视频的演讲者表示Francesc Campoy,在使用切片时可以放心地使用nil切片而不需要担心其容量问题,因为它的每次重分配容量都是倍增的。即nil切片的第一次append,会重分配一个容量为1的切片。而后会分配容量为2/4/8/16这样倍增的数值,所以长度为1000的切片也只是重分配了10次。如果切片的内存重分配确实影响了应用的性能,那可以考虑声明一个具有一定容量的切片,否则,使用nil切片,因为他们通常足够快
Golang之nil的妙用_第4张图片

nil映射(map)

nil映射是指未初始化的映射,其长度为0,可读但不可写,如:

var m map[string]string
len(m) // 0
for key,value := range m // 迭代0次
value,ok := m["key"] // "",false
m["key"]="value" // panic: assignment to entry in nil map

nil映射用在只读的地方非常方便,假设有一个创建Get请求的函数:

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
}

如果你不想设置该请求的header,那你只需要传入nil:

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

不需传入一个空的映射,如:

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

Golang之nil的妙用_第5张图片

nil通道

nil通道是未初始化的通道,当尝试写入或者读取时,会永久阻塞,且无法被close。如:

var c chan os.Signal
<-c // 永久阻塞
c <- x // 永久阻塞
close(c) // panic close of nil channel

nil通道的永久阻塞的特性在某些场景非常有用。视频举例了一个合并通道的栗子,由于栗子比较长,我这里只简单分析其使用场景。感兴趣的可以看看视频,这一段从第22分钟开始,讲得很风趣幽默,挺搞笑的。

在讲nil通道的使用场景前,我们先看看关闭的通道的特性。关闭的通道无法发送数据,接收数据时值为"0"、false,无法close:

var c chan os.Signal
v, ok <- c // nil, false
c <- v // panic: send on closed channel
close(c) // panic: close of nil channel

视频中讲nil通道的场景是,当我们使用select时:

select {
  case v,ok := <-a:
    doSomething...
  case v,ok := <-b:
    doSomething...
}

如果通道a或者b的其中一个被关闭,即close(a)或者close(b),那么,select会陷入死循环,不停地从关闭的通道中读取到"0",false,不停地执行doSomething,最后你们的数据中心烧起来了…

nil通道就能阻止这种事情的发生,当我们希望一个通道停止读写时,我们可以直接设置其为nil,如:

select {
  case v,ok := <-a:
    if !ok{
      a = nil
    }
    doSomething...
  case v,ok := <-b:
    if !ok{
      b = nil
    }
    doSomething...
}

这样就能避免数据中心烧成狗了。

nil函数

由于在Go语言中,函数可以作为结构体的域(Field)存在,所以必须为其设置一个初始值,那就是nil:

type Foo struct{
  f func() error // 初始值为nil
}

nil函数可以用于懒加载或者执行默认操作,如:

func NewServer(logger func(string,...interface{})){
  if logger == nil{
    logger = log.Printf // 使用默认logger
  }
  logger("init ... ")
}

nil接口

nil接口最为常用的场景是作为一个信号,想必写过很多Go代码的已经见过无数次了,如:

if err != nil {
  ...
}

需要注意的是空指针并不等于空接口:

var ps *string // nil
var i interface{} // nil
ps == i // false

总结

Golang之nil的妙用_第6张图片
Golang之nil的妙用_第7张图片
Golang之nil的妙用_第8张图片

欢迎关注我的个人公众号一起交流~
Golang之nil的妙用_第9张图片

你可能感兴趣的:(Go)