GO语言中ni,零值与空结构体的使用
作者:飞空之羽
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: }
到此这篇关于GO语言中ni,零值与空结构体的文章就介绍到这了,更多相关GO ni 零值 空结构体内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!