25

深入理解nil

 5 years ago
source link: https://studygolang.com/articles/18282?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

9.2 深入理解nil

nil 是Go中熟悉且重要的预先声明的标识符。它是多种类型零值的字面表示。许多具有其他一些流行语言经验的新Go程序员可能会将其 nil 视为 null (或 NULL )其他语言的对应物 。这部分是正确的,但 nil 在Go和 null (或 NULL )其他语言之间存在许多差异。

按照Go语言规范,任何类型在未初始化时都对应一个零值:布尔类型是false,整型是0,字符串是"",而指针,函数,interface,slice,channel和map的零值都是nil。

nil 没有默认类型

Go中的每个其他预先声明的标识符都具有默认类型。例如,

  • 默认类型为 truefalse 都是 bool 类型。
  • 默认类型 iotaint

但是 nil 它没有默认类型,尽管它有许多可能的类型。编译器必须有足够的信息来 nil 从上下文中推导出值的类型 。

示例:

package main

func main() {
	// This following line doesn't compile.
	/*
	v := nil
	*/

	// There must be sufficient information for compiler
	// to deduce the type of a nil value.
	_ = (*struct{})(nil)
	_ = []int(nil)
	_ = map[int]bool(nil)
	_ = chan string(nil)
	_ = (func())(nil)
	_ = interface{}(nil)

	// This lines are equivalent to the above lines.
	var _ *struct{} = nil
	var _ []int = nil
	var _ map[int]bool = nil
	var _ chan string = nil
	var _ func() = nil
	var _ interface{} = nil
}

nil Go中是一个预先声明的标识符

您可以在 nil 不声明的情况下使用它。

nil 可以表示多种类型的零值

在Go中, nil 可以表示以下类型的零值:

  • pointer types (including type-unsafe ones).
  • map types.
  • slice types.
  • function types.
  • channel types.
  • interface types.

示例:

package main

import "fmt"

type Person struct {
	Id   int
	Name string
	Info interface{}
}

func main() {
	var p Person
	fmt.Println(p)// {0  <nil>}
}

nil 在Go中不是关键字

预先宣布的 nil 可以被遮蔽。

示例:

package main

import "fmt"

func main() {
   nil := 123
   fmt.Println(nil) // 123

}

nil 具有不同种类的价值的大小可能不同

一个类型的所有值的内存布局总是相同的。 nil 类型的值不是例外。 nil 值的大小始终与其类型与 nil 值相同的非零值的大小相同。因此, nil 表示不同类型的不同零值的标识符可以具有不同的大小。

示例:

package main

import (
	"fmt"
	"unsafe"
)

func main() {
	var p *struct{} = nil
	fmt.Println( unsafe.Sizeof( p ) ) // 8

	var s []int = nil
	fmt.Println( unsafe.Sizeof( s ) ) // 24

	var m map[int]bool = nil
	fmt.Println( unsafe.Sizeof( m ) ) // 8

	var c chan string = nil
	fmt.Println( unsafe.Sizeof( c ) ) // 8

	var f func() = nil
	fmt.Println( unsafe.Sizeof( f ) ) // 8

	var i interface{} = nil
	fmt.Println( unsafe.Sizeof( i ) ) // 16
}

nil 使用场景

pointers

nil pointer

  • 指向 nil, 又名 nothing
  • pointer 的零值
var p *int  // 声明一个 int 类型的指针
println(p)  // <nil>
p == nil    // true
*p          // panic: runtime error: 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 了,无需重复判断。换成打印二叉树的值或者查找二叉树的某个值都是一样的:Coding Time

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 []T
len(s) // 0
cap(s) // 0
for range s {
} // iterates zero times
s[i] // panic: index out of range

一个为 nilslice ,除了不能索引外,其他的操作都是可以的, slice 有三个元素,分别是长度、容量、指向数组的指针,当你需要填充值的时候可以使用 append 函数, slice 会自动进行扩充。所以我们并不需要担心 slice 的大小,使用 append 的话 slice 会自动扩容。

var s []int
for i := 0; i < 10; i++ {
    fmt.Printf("len: %2d cap: %2d\n", len(s), cap(s))
    s = append(s, i)
}

那么为 nil 的slice的底层结构是怎样的呢?根据官方的文档,slice有三个元素,分别是长度、容量、指向数组的指针:

BBrQrib.png!web

NB7buav.png!web

map

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

// nil maps
var m map[t]u

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

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

func NewGet(url string, headers map[string]string) (*http.Request, error) {
    req, er := 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)

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

channels

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

closed channels

var c chan t
v, ok <- c      // zero(t), false
c <- x          // panic: send on closed channel
close(c)        // panic: close of nil channel

如果在外部调用中关闭了 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 {
                a = nil
                fmt.Println("b is nil")
                continue
            }
            out <- v
        }
    }
    fmt.Println("close out")
    close(out)
}

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

functions

函数可以被用作结构体字段, 逻辑上,默认的零值为 nil

type Foo struct {
    f func() error
}

nil funcs for default values

lazy initialization of variables, nil can also imply default behavior

func NewServer(logger func(string, ...interface{})) {
    if logger == nil {
        logger = logger.Printf
    }
    logger("initializing %s", os.Getenv("hostname"))
    // ...
}

interfaces

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
}

输出结果:false. do 函数声明了一个 *doError 的变量 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 .

在Go中, nil 只是一个标识符,可用于表示某些类型的零值。它不是单一的价值。相反,它可以表示具有不同

存储器布局的许多值。

links

  • 目录
  • 上一节:
  • 下一节:

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK