认识GO语言中的nil,零值与空结构体

go语言的初学者,特别是java开发者新学习go语言,对于一些和java类似但是又有差异的概念很容易混淆,比如说go中的零值,nil 和 空结构体。本文就来详细探讨一下go中这些特殊概念的含义和实际场景中的应用:

零值

零值(The Zero Value)可以看作为当你声明了一个变量,但没有显式的初始化的时候,系统为变量赋予的一个默认初始值。官方对零值的定义如下:

When storage is allocated for a variable, either through a declaration or a call of new, or when a new value is created, either through a composite literal or a call of make, and no explicit initialization is provided, the variable or value is given a default value. Each element of such a variable or value is set to the zero value for its type: false for booleans, 0 for numeric types, “” for strings, and nil for pointers, functions, interfaces, slices, channels, and maps. This initialization is done recursively, so for instance each element of an array of structs will have its fields zeroed if no value is specified.

据此我们可总结出:

  • 值类型 布尔类型为 false, 数值类型为 0,字符串为”“,数组和结构体(struct)会递归初始化其元素或字段,即其初始值取决于元素或字段。这里所谓的值类型其实就相当于java中的 primary 类型,只是需要注意的是string在java中是对象类型,而go中string则是值类型。

  • 引用类型 均为 nil,包括指针 pointer,函数 function,接口 interface,切片 slice,管道 channel,映射 map。

tip: 其实go里面没有真正的引用类型,可以粗略的理解为值类型的变量直接存储值,引用类型的变量存储的是一个地址,这个地址用于存储最终的值

值类型

因为有零值的存在,使得我们在使用变量时,大部分情况下可以不必进行初始化而直接使用,这样能够保持代码的简洁性,也能够尽量避免出现Java开发中常见的**NullPointerException,**以下是一些例子:

package main

import "sync"

type Value struct {
    mu sync.Mutex   //无需初始化,声明就能用
    val int
}

func (v *Value)Incr(){
    defer v.mu.Unlock()
    v.mu.Lock()
    v.val++
}

func main() {
    var i Value
    i.Incr()
}

sync.Mutex本质上是一个结构体:

// A Mutex is a mutual exclusion lock.
// The zero value for a Mutex is an unlocked mutex.
//
// A Mutex must not be copied after first use.
type Mutex struct {
	state int32
	sema  uint32
}

那么如果是引用类型,零值为nil,是不是就不能直接用了呢?这个实际上也要分情况,按照类型我们一个个来看:

切片(Slices)

切片的零值是一个nil slice,除了不能按照序号索引查询以外,其它的操作都能做:

func testNilSlice() {
	var nilSlice []string
    fmt.Println(nilSlice == nil) // true
    fmt.Println(nilSlice[0]) //index out of range
	emptySlice = append(nilSlice, "dd") // append操作会自动扩容
	fmt.Println(nilSlice[0]) //输出dd
}

nil slice与not nil slice的区别:

type Person {
  Friends []string
}

var f1 []string //nil切片
json1, _ := json.Marshal(Person{Friends: f1})
fmt.Printf("%s\n", json1) //output:{"Friends": null}

f2 := make([]string, 0) //non-nil空切片 ,等价于 f2 := []string{}
json2, _ := json.Marshal(Person{Friends: f2})
fmt.Printf("%s\n", json2) //output: {"Friends": []}

推荐在日常使用时,没有特殊需求都使用var nilSlice []string 这样的形式声明空切片:https://github.com/golang/go/wiki/CodeReviewComments#declaring-empty-slices

Map

对于nil的map,我们可以简单把它看成是一个只读的map,不能进行写操作,否则就会panic:

func testNilMap() {
	var m map[string]string
	fmt.Println(m["key"]) //输出""
    m["key"]="value" //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
}

//调用该方法时如果没有header,可以传入一个空的map,例如:
NewGet("http://google.com", map[string]string{})
//也可以直接传入nil
NewGet("http://google.com", nil)

Channel

nil channel会阻塞对该channel的所有读、写。所以,可以将某个channel设置为nil,进行强制阻塞,对于select分支来说,就是强制禁用此分支

func addIntegers(c chan int) {
    sum := 0
    t := time.NewTimer(time.Second)
    for {
        select {
            case input := <-c:
                sum = sum + input
            case <-t.C:
                c = nil
                fmt.Println(sum)  // 输出10
        }
    }
}

func main() {
	c := make(chan int, 1)
	go addIntegers(c)
	for i := 0; i <= 10; i++ {
		c <- i
		time.Sleep(time.Duration(200) * time.Millisecond)
	}
}

指针(Pointers)

指针如果为nil,则对指针进行解引用的话,会引发我们在java中非常熟悉的空指针错误

type Person struct {
	Name string
	Sex  string
	Age  int
}

var p *Person
fmt.Println(p.Name)  // panic: runtime error: invalid memory address or nil pointer dereference

神奇的nil

nil 是 Golang 中预先声明的标识符,其主要用来表示引用类型的零值(指针,接口,函数,映射,切片和通道),表示它们未初始化的值。

// [src/builtin/builtin.go](https://golang.org/src/builtin/builtin.go#L98)
//
// nil is a predeclared identifier representing the zero value for a
// pointer, channel, func, interface, map, or slice type.
var nil Type // Type must be a pointer, channel, func, interface, map, or slice type

nil在go语言里面不是一个关键字或者保留字,所以你可以用nil作为变量名(作死):

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

nil没有默认的类型,所以不能给一个未声明类型的变量赋值,也不能和自己比较:

a := nil
// cannot declare variable as untyped nil: a
fmt.Println(nil == nil)
// invalid operation: nil == nil (operator == not defined on nil)
fmt.Printf("%T", nil)
// use of untyped nil

比较nil时一定要注意nil实际上是有类型的,不同类型的nil是不相等的,比如下面的例子:

var p *int
var i interface{}

fmt.Println(p)      // <nil>
fmt.Println(i)      // <nil>

fmt.Println(p == i) // false

再看一个在实际编码里面很容易犯的错误:

type BusinessError struct {
	error
	errorCode int64
}

func doBusiness() *BusinessError {
	return nil
}

func wrapDoBusiness() error {
	err := doBusiness()
	return err
}

func testError() {
	err := wrapDoBusiness() //这里面拿到的本质上是一个<T:*BusinessError,V:nil>的nil
	fmt.Println(err == nil)
}

建议:如果任何地方有判断interface是否为 nil 值的逻辑,一定不要写任何有关于将interface赋值为具体实现类型(可能为nil)的代码,如果是 nil 值就直接赋给interface,而不要过具体类型的转换

type BusinessError struct {
	error
	errorCode int64
}

func doBusiness() *BusinessError {
	return nil
}

func wrapDoBusiness() error {
	err := doBusiness() 
	if err == nil {
		return nil  //如果返回值为nil,直接返回nil,不要做类型转换
	} else {
		return err
	}
}

func testError() {
	err := wrapDoBusiness()
	fmt.Println(err == nil)
}

空结构体

golang 正常的 struct 就是普通的一个内存块,必定是占用一小块内存的,并且结构体的大小是要经过边界,长度的对齐的,但是“空结构体”是不占内存的,size 为 0;

var q struct{}
fmt.Println(unsafe.Sizeof(q)) // 0

空结构体 struct{ } 为什么会存在的核心理由就是为了节省内存。当你需要一个结构体,但是却丝毫不关系里面的内容,那么就可以考虑空结构体。以下是几个经典的用法:

map & struct{}

map 和 struct {} 一般的结合姿势是这样的:

// 创建 map
m := make(map[int]struct{})
// 赋值
m[1] = struct{}{}
// 判断 key 键存不存在
_, ok := m[1]

一般 map 和 struct {} 的结合使用场景是:只关心 key,不关注值。比如查询 key 是否存在就可以用这个数据结构,通过 ok 的值来判断这个键是否存在,map 的查询复杂度是 O(1) 的,查询很快。这种方式在部分场景下可以起到类似Java中Set的作用

chan & struct{}

channel 和 struct{} 结合是一个最经典的场景,struct{} 通常作为一个信号来传输,并不关注其中内容。chan 本质的数据结构是一个管理结构加上一个 ringbuffer ,如果 struct{} 作为元素的话,ringbuffer 就是 0 分配的。

chan 和 struct{} 结合基本只有一种用法,就是信号传递,空结构体本身携带不了值,所以也只有这一种用法啦,一般来说,配合 no buffer 的 channel 使用。

waitc := make(chan struct{})

// ...
goroutine 1:
    // 发送信号: 投递元素
    waitc <- struct{}
    // 发送信号: 关闭
    close(waitc)

goroutine 2:
    select {
    // 收到信号,做出对应的动作
    case <-waitc:
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值