Go nil详解

Go 类型的默认值

基本类型(basic type)

  • 内置字符串类型:string.
  • 内置布尔类型:bool.
  • 内置数值类型:
    • int8uint8byte)、int16uint16int32rune)、uint32int64uint64intuintuintptr
    • float32float64
    • complex64complex128

注意,byteuint8的一个内置别名,runeint32的一个内置别名。

组合类型(composite type)

Go支持下列组合类型:

  • 指针类型- 类C指针
  • 结构体类型 - 类C结构体
  • 函数类型 - 函数类型在Go中是一种一等公民类别
  • 容器类型,包括:
    • 数组类型 - 定长容器类型
    • 切片类型 - 动态长度和容量容器类型
    • 映射类型(map)- 也常称为字典类型。在标准编译器中映射是使用哈希表实现的。
  • 通道类型- 通道用来同步并发的协程
  • 接口类型- 接口在反射和多态中发挥着重要角色

不同类型的默认值

类型 默认值
bool false
numbers 0
string ""
pointer nil
slice nil
map nil
channel nil
function nil
interface nil

值得注意的是 struct类型和数组类型零值不是nilStruct 是各字段值为对应类型的零值,数组类型是各元素类型的零值。且不能将struct类型和数组类型和与nil`进行等值判断,语法校验不通过。

不同类型变量的声明与初始化

基本类型声明时就会初始化为默认零值。组合类型则分为声明和初始化两阶段。因此只需要讨论组合类型。统一的理解是,只声明未初始化的组合类型变量都是nil。不过其中还有一些区别,下面一一讲解。

// 声明
var a *int
var b *string
var c []int
var d map[string]string
var e chan int
var f func()
// 初始化
a = make([]int,2)

// 声明+初始化

c := make([]int,2)
d := make(map[string]string)
e := make(chan int)


c := []int{1,2,3,4}
d := map[string]string{"aaa":"bbb"}

c := &[]int{1,2,3,4}
d := &map[string]string{"aaa":"bbb"}

组合类型nil

nil pointer

var w *int  //对应汇编 MOVQ    $0, "".w+48(SP)
// 此时未初始化。 w为nil
if w == nil {
    fmt.Println("w is nil")
}

从汇编中可以看到 nil pointer 其实就是指向内存为0的指针。

对指针的操作有两种

  1. 取值 *w, 对nil 指针取值会 panic。
  2. 调用 nil 指针对应的方法,,可以正常调用

nil slice

slice 分为两部分,一部分是slice本身的结构如下图,其中ptr指向具体元素。

make([]byte, 0)

make([]byte, 5)

一般都会区分两个概念,nil sliceempty slice

nil slice ,其中ptr声明未初始化,为nil。 和nil比较为true

empty slice,其中ptr声明并初始化,但是指向空间未分配。和nil比较为false

var w []int
// 汇编代码
// 设置 ptr 为0
// MOVQ    $0, "".w+112(SP)
// XORPS   X0, X0
// 设置 len 和 cap 为0
// MOVUPS  X0, "".w+120(SP)

w == niltrue

w1 := make([]int, 0)
// 汇编代码,功能同上
// MOVQ    AX, "".w1+88(SP)
// XORPS   X0, X0
// MOVUPS  X0, "".w1+96(SP)


w1 == nilfalse

从汇编代码能清晰的看到,nil sliceempty slice都初始化了slice结构,只是ptr的值不同。所以 声明一个nil slice后 可以直接使用

nil slice的操作,和empty slice相同:

可以进行 lencapfor rangeappend。注意slice没有删除元素的方法,只能通过在此slice上新建slice来完成

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

append(s,1) // no eror

nil map

和上述一样,也有两个概念nil mapempty map

  • map声明后初始化前,此时为nil map
  • map声明后不能进行赋值,只有初始化后才能进行赋值操作,初始化后为empty map
var w map[int]string   // nil map
// 汇编
// MOVQ    $0, "".w+64(SP)
w == niltrue

w1 := map[int]string{} // empty map
// 汇编代码太长,省略
w1 == nilfalse

从汇编中可以发现,nil map仅仅声明了一个指针,并置为nil,而empty map则是声明了对应的数据结构。

因此 nil map在进行赋值操作前,必须初始化,这是与slice的不同点。

nil mapempty map可进行的操作:

  • nil map可进行查找(查找任意值会返回数据类型的默认值)、删除、len和range操作,并不会报错 。但是不能进行赋值操作,必须初始化。

  • empty map, 可进行map所有操作。

  • 只声明一个map类型变量时,为nil map

  • 此时为只读map,无法进行写操作,否则会触发panic

  • map 构建分两步,

    • map声明后初始化前,可进行查找、删除、len和range操作,并不会报错 ,此时为nil map
    • map声明后不能进行赋值,只有初始化后才能进行赋值操作,初始化后为empty map
  • nil mapempty map区别:

    • nil map:只声明未初始化,此时为只读map,不能写入操作,示例:var m map[t]v
    • empty map:空map,已初始化,可写入,示例:m := map[t]v{}m := make(map[string]string, 0)
// nil maps
var m map[t]u // nil map
m2 := map[t]u{} // empty map
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 channel

var w chan int       // nil chan
// 汇编代码
// MOVQ    $0, "".w+56(SP)

w1 := make(chan int) 
// 汇编代码
// LEAQ    type.chan int(SB), AX
// MOVQ    AX, (SP)
// MOVQ    $0, 8(SP)
// PCDATA  $1, $1
// CALL    runtime.makechan(SB)
// MOVQ    16(SP), AX
// MOVQ    AX, "".w1+48(SP)

从汇编可以看到 channel 也需要声明和初始化。nil chan 仅仅只是创建了一个为nil指针,初始化才会创建具体的结构体。

nil chan 的操作:

  1. 读 写 都会无限阻塞
  2. close 会发生panic
// nil channels
var c chan t 
<- c      // blocks forever
c <- x    // blocks forever
close(c)  // panic: close of nil channel

nil func

mapchannelfunction的本质都是指向具体实现的指针,而对应类型的nil则是不指向任何地址。因此不做具体介绍

nil interface

interface底层由两部分组成:类型、值(type, value),当二者均为nil时,此时interface才为nil。

var w io.Reader 此时为(nil,nil)
// 汇编
// XORPS   X0, X0
// MOVUPS  X0, "".w+40(SP)
w == nil  // true 

var a *strings.Reader 此时为(*strings.Reader,nil)
w = a
// 汇编
MOVQ    $0, ""..autotmp_2+32(SP)
LEAQ    go.itab.*strings.Reader,io.Reader(SB), AX
MOVQ    AX, "".w+40(SP)  // _type 为 *strings.Reader
MOVQ    $0, "".w+48(SP)  // datac 为  nil


w = strings.NewReader("1234")  此时为(*strings.Reader, 对应数据地址)
// 汇编
LEAQ    go.string."1234"(SB), AX
MOVQ    AX, (SP)
MOVQ    $4, 8(SP)
PCDATA  $1, $0
CALL    strings.NewReader(SB)
MOVQ    16(SP), AX    //strings.NewReader(SB) 返回的地址 放入 AX
MOVQ    AX, ""..autotmp_1+32(SP)
LEAQ    go.itab.*strings.Reader,io.Reader(SB), CX
MOVQ    CX, "".w+40(SP) // _type 为 *strings.Reader
MOVQ    AX, "".w+48(SP) // datac 为 strings.NewReader(SB) 返回的地址

var s Write // Write 此时 s 为nil,为一个纯接口

var p *Person           // nil of type *Person
var s Write = p  // 赋值后,s 带有了p的类型信息,但是 p 为nil,所以值为nil


有可得,当interface 为 nil时,变量为一个nil的指针;当不为nil值时,会实例化为下面一种结构体。

type eface struct { // 16 字节 不包含任何方法的接口
    _type *_type
    data  unsafe.Pointer
}
type iface struct { // 16 字节 包含方法的接口
    tab  *itab
    data unsafe.Pointer
}

不要返回具体的错误类型,而应直接返回nil

下面展示返回类型为interface时的差异:

错误示例:

func do() error {   // error(*doError, nil)
  var err *doError
  return err  // nil of type *doError
}

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

正确示例:

func do() *doError {   // nil of type *doError
  return nil
}

func main() {
  err := do()             // nil of type *doError
  fmt.Println(err == nil) // true
}

再看下面这段代码,虽然do()返回nil,但wrapDo()返回依然是接口,也就是类型为*doError,值为nil的接口,此时拿到的返回值并不等于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
}

nil的有效利用

  • nil类型接收者是可以正确调用方法的 nil reveivers are userful
  • Keep nil (pointer) useful if possible, if not NewX()
  • Use nil slices, they're often fast enough
  • Use nil maps as read-only empty maps,将nil map作为只读的空map(不能读nil map进行写入操作,否则会发生panic)
  • Use nil channel to disable a select case,nil channel来阻塞selct/case语句
  • nil value can satisfy interface,不同类型的nil值可满足interface,也就是可赋值给interface
  • Use nil interface to signal defaul,使用nil的interface来标识使用缺省处理

nil的一些常见用法

nil pointer用法

  • nil pointer用来和nil比较确认是否为零值
var p *int
p == nil // true
*p // panic: runtime error: invalid memory address or nil pointer dereference

来看看,如何实现二叉树树的求和操作:

type tree {
  v int
  l *tree
  r *tree
}

func (t *tree) Sum() int

第一种方案,有两个问题:

  • 代码冗余,重复的if v != nil {v.m()}
  • t为nil时,会发生panic
var t *tree // nil of type *tree
sum := t.Sum() // panic
// 实现方案1
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
}

nil接收者,也可以正确调用方法,所以可以利用这一点,改造出方案2:

// 方案2:代码简洁、可断性提高很多
func (t *tree) Sum() int {
  if t == nil {
    return 0
  }
  return v + t.l.Sum() + t.r.Sum()
}

通过利用类型的nil,可以灵活实现是否为空的处理,已经便捷实现扩展函数:

func (t *tree) String() string {
  if t == nil {
    return ""
  }
  return fmt.Sprint(t.l, t.v, t.r)
}

func (t *tree) Find(v int) bool {
  if t == nil {
    return false
  }
  return t.v go== v || t.l.Find(v) || t.r.Find(v)
}

nil slices用法

  • 不能对nil slice进行取值,否则会发生panic
  • 可通过append函数对nil slice进行增加元素操作
var s []int
len(s) // 0
cap(s) // 0
for range s // 执行0次
s[i] // panic: index out of range

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

nil map用法

  • nil map不能进行增加元素操作,因它还没有进行初始化
  • nil map作为只读的空map
var m map[t]u
len(m) // 0
for range m // 执行0次
v, ok := m[i] // v=zero(u), ok=false
m[i] = x      // panic: assignment to entry in nil map
// 有个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
}

// 调用时,传递headers
NewGet(
  "http://google.com",
  map[string]string{
    "USER_AGENT":"google/gopher",
  },  // go语言五十度灰,如果参数和)之间换行形式,参数尾部需追加,逗号
)

// 调用时,不传递headers,可以传递一个empty map空map
NewGet(
  "http://google.com",
  map[string]string{}, // 传递空map,empty map
)

// 调用时,不传递headers,可以传递一个nil map
NewGet(
  "http://google.com",
  nil, // 传递nil map,也是合法的
)

nil channel用法

  • 不能对nil channel进行close()操作,发触发panic: close of nil channel
  • 不能对channel进行多次close,会触发 panic: close of closed channel
  • 关闭channel,在select/case中将依然能获取到值,但nil channel将阻塞读操作来失效select/case中逻辑
var c chan t // nil of type chan t
// nil channel操作时
<- c // block forever,持续阻塞
c <- x // block forever,持续阻塞
close(c) // panic: close of nil channel,关闭nil channel发生panic

// 对于已关闭的channel,将发生如下现象
v := <- c  //closed channel是可以被消费者继续读取的,在读完了有意义的数据之后,将读到一堆空值。比如这里的int类型就是0。
v, ok := <-c  // zero(t), false 不会阻塞,返回零值和False
c <-x // panic: send to close channel
close(x) // panic: close of closed channel,备注原文中错误

现在假设要实现一个合并函数,实现从两个通道中获取数据,然后写入out输出通道:


func merge(out chan<- int, a,b <-chan int)  {
  for {
    select {
      case v:= <-a: // 当a/b通道关闭时,这里将持续获取到0
        out <-v
      case v:= <-b
        out <-v
    }
  }

}

改造代码后如下:

func merge(out chan<- int, a,b <-chan int)  {
  var aClosed, bClosed bool

  for !aClosed || !bClosed {
    select {
    case v,ok := <-a: // 此时通道关闭后,就不会再进行获取了
      if !ok {
        aClosed = true
        fmt.Println("a is closed")
        continue
      }
      out <-v
    case v,ok := <-b:
      if !ok {
        bClosed = true
        fmt.Println("b is closed")
        continue
      }
      out <-v
    }
  }
  close(out) // 需要在不使用后进行close操作
}

终于搞定了,提交代码,转交给测试吧!你会发现“a is closed” 和 “b is closed”可能会执行多次,为什么?因为外部逻辑如果关闭了a/b,此时还能够读取。此时上述代码中会有空转的逻辑,运行下,看看打印输出:

a is closed
a is closed
a is closed
a is closed
a is closed // 很多次无用的空转逻辑
b is closed // 最后一次,a/b都未空时才结束循环

可能看到这里已经有些蒙了,但要明白,无论是否关闭一个chanel,都可以从中读取到值,而如果不需要对channel取值操作了,那么可以将其改为nil,这样会永远阻塞读,防止再发生读操作;同时应在入口增加非nil判断。

func merge(out chan<- int, a, b <-chan int) {
    for a != nil || b != nil {
        select {
        case v, ok := <-a: // 此时通道关闭后,a == nil,在读取会永远阻塞
            if !ok {
                a = nil
                fmt.Println("a is closed")
                continue
            }
            out <- v
        case v, ok := <-b:
            if !ok {
                b = nil
                fmt.Println("b is closed")
                continue
            }
            out <- v
        }
    }
    close(out) // 需要再不使用后进行close操作
}

nil func用法

  • go中函数是一等公民
  • 函数可以作为struct结构体的字段,缺省值则为nil
type Foo struct {
  f func() error // f is type of func() error
}

// 常见用法,传输函数为nil,增加缺省处理
func NewServer(logger func(string, ...interface{})) {
  if logger == nil {
    logger = log.Printf
  }
  logger.Printf("initializng %s", os.Getenv("hostname"))
}

nil interface用法

  • nil interface作为一种信号来使用
  • nil指针,不等于nil接口
if err != nil {
  ...
}
type Summer interface {
  Sum() int
}

var t *tree // nil of type *tree
var s Summer = t // nil指针,可以是合法的interface类型的值
// 此时,对接接口类型变量s而言,其类型为*tree,值为nil,也就是说(*tree, nil)行的interface

fmt.Println(t==nil, s.Sum()) // true, 0

type ints []int
func (i ints) Sum() int {
  s := 0
  for _, v := range i{
    s += v
  }
  return s
}

var i ints
var s Summer = i // summer 为([]int,i)
fmt.Println(s==nil, s.Sum()) // false, 0
// 通过判断接口为nil,来给定缺省值
func doSum(s Summer) int {
  if s == nil {
    return 0
  }
  return s.Sum()
}

var t *tree
doSum(t) // interface的类型和值分别为:(*tree, nil)

var i ints
doSum(i) // (ints, nil)

doSum(nil) // (nil, nil)

http.HandleFunc("localhost:8080", nil) // 传递nil,则使用缺省处理

参考阅读

not found!