教程集 www.jiaochengji.com
教程集 >  Golang编程  >  golang教程  >  正文 【golang】nil的理解

【golang】nil的理解

发布时间:2022-03-15   编辑:jiaochengji.com
教程集为您提供【golang】nil的理解等资源,欢迎您收藏本站,我们将为您提供最新的【golang】nil的理解资源

最近在油管上面看了一个视频:Understanding nil,挺有意思,这篇文章就对视频做一个归纳总结,代码示例都是来自于视频。

<h1>nil是什么</h1>

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

<pre><code>if err != nil {    // do something....} </code></pre>

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

<pre><code>bool -> false                   numbers -> 0                   string    -> ""       pointers -> nil slices -> nil maps -> nil channels -> nil functions -> nil interfaces -> nil </code></pre>

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

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

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

<pre><code>type Type int var nil Type </code></pre>

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

<pre><code>var nil = errors.New("hi") </code></pre>

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

<h1>nil有什么用</h1>

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

<h2>pointers</h2><pre><code>var p *int p == nil    // true *p          // panic: invalid memory address or nil pointer dereference </code></pre>

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

<pre><code>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 } </code></pre>

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

<pre><code>if v != nil { v.m() } </code></pre>

另一个是当<code>t</code>是<code>nil</code>的时候会panic:

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

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

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

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

<pre><code>func(t *tree) Sum() int {   if t == nil {       return 0   }   return t.v t.l.Sum() t.r.Sum() } </code></pre>

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

<pre><code>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) } </code></pre>

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

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

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

当有元素的时候:
<span class="img-wrap"></span>
slice

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

<h2>map</h2>

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

<pre><code>// 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 </code></pre>

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

<pre><code>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 } </code></pre>

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

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

或者这样传:

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

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

<pre><code>NewGet("http://google.com", nil) </code></pre>

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

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

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

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

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

<pre><code>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) } </code></pre>

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

<h2>interface</h2>

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

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

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

<pre><code>func do() error {    return nil } </code></pre>

再来看看这个例子:

<pre><code>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 } </code></pre>

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

<h1>总结</h1>

看完了那个视频,发现<code>nil</code>还有这么多用处,真是意外之喜。
油管上面还有很多干货满满的视频,可以多学习学习咯。

到此这篇关于“【golang】nil的理解”的文章就介绍到这了,更多文章或继续浏览下面的相关文章,希望大家以后多多支持JQ教程网!

您可能感兴趣的文章:
Golang interface(接口)的nil判断
【golang】nil的理解
golang json文件存取
golang 上传文件(包括 gin 实现)
golang 面试题(十三)interface内部结构和nil详解
golang基础学习-AES加密
golang http上传zip文件,自动解压到目录
Golang中seek使用方法详解
golang 字符串分割_Golang 微服务教程(四)
GO实现文件压缩算法

[关闭]
~ ~