在go语言中nil只是一个变量名,buildin/buildin.go 中 定义
// 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可以表示pointer,channel,func,interface,map,slice的零值
nil也是有类型的,比如 (*int)(nil) (interface{})(nil) ,他们都是不同类型,也不相等。
nil的比较
var a = nil 这是编译错误的,因为编译器不知道nil是什么类型。但是nil与一些特定类型比较,比如 a == nil。这是因为nil 在于不同类型比较的时候会有不同的逻辑。下面是不同类型的比较
- pointer
nil pointer是一个没有指向任何值的指针,它的值是0x0。var a = (*int)(unsafe.Pointer(uintptr(0x0))) print(a == nil) //true
恭喜我们人工创造了一个 nil pointer !
-
slice
一个slice由3部分组成,pointer,len和cap,pointer指向一段连续的数组,len长度,cap容量。
当pointer是nil,len和cap都是0的时候,这个slice等于nil. 下面做个实验
var a = []int{} print(a==nil) //false type aa struct { ptr unsafe.Pointer len int cap int } aaa := (*aa)(unsafe.Pointer(&a)) aaa.ptr = nil print(a==nil) //true
略微有点黑科技。简单来说,我们原本声明了一个
empty slice
, empty slice是不等于nil。但是我们把这个slice结构体中的ptr改成了nil,于是这个slice就变成了nil slice
. -
interface
这个已经说过,当一个interface的type和value都是nil的时候,这个interface才等于nil. 这真的是个坑人无数的golang陷阱,这里就再举一个小栗子好了。
type A interface{} type B struct{} var a A = (*B)(nil) print(a == nil) //false a = nil print(a == nil) //true
student := myprogram.Student{
Mame: "",
Age: 0,
Email: "",
}
var a interface{} = student
// 类型断言 针对空接口,判断某个接口的动态类型是否是某个特定类型
if i,ok:= a.(myprogram.Args) ;ok{
fmt.Printf("%T %v",i,i)
}
//主要针对 非空接口,判断Student是否实现了Interface1接口
var _ Interface1 = (myprogram.Student)(nil)
package myprogram
type Args struct {
X,Y int
}
type Student struct {
Mame string `json:"mame" form:"fname"`
Age int `json:"jage" from:"fage"`
Email string
}
/*func (s Student) Error() string {
//panic("implement me")
}*/
func (s Student) RuntimeError() {
panic("implement me")
}
func (s Student) Change(i int) {
s.Age = i
}
func (s *Student) Change2(i int) {
s.Age = i
}
func (s Student) Equal(i int) bool {
//panic("implement me")
return s.Age == i
}
- map,function,channel都是特殊的指针,指向各种特定的实现。
对于nil
的map,我们可以简单把它看成是一个只读的map,不能进行写操作,否则就会panic。
对于nil 的channel 输入输出永远是阻塞的,关闭引发panic。
// 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
}
}
}
如果在外部调用中关闭了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 {
b = nil
fmt.Println("b is nil")
continue
}
out <- v
}
}
fmt.Println("close out")
close(out)
}
在知道channel关闭后,将channel的值设为nil,这样子就相当于将这个select case子句停用了,因为nil
的channel是永远阻塞的。
聊一聊 Go 语言中的零值,它有什么用?
脑子进煎鱼了 今天
编者荐语:
asong,非科班程序员,现就职于某电商外企,专研Go语言、微服务系统设计,欢迎关注 asong 的公众号,一起学习进步!以下文章来源于Golang梦工厂 ,作者AsongGo
背景
哈喽,大家好,我是asong
。今天与大家聊一聊Go语言中的零值。大学时期我是一名C
语言爱好者,工作了以后感觉Go
语言和C
语言很像,所以选择了Go
语言的工作,时不时就会把这两种语言的一些特性做个比较,今天要比较的就是零值特性。熟悉C
语言的朋友知道在C
语言中默认情况下不初始化局部变量。未初始化的变量可以包含任何值,其使用会导致未定义的行为;如果我们未初始局部变量,在编译时就会报警告 C4700,这个警告指示一个Bug
,这个Bug
可能导致程序中出现不可预测的结果或故障。而在Go语言就不会有这样的问题,Go语言的设计者吸取了在设计C
语言时的一些经验,所以Go
语言的零值规范如下:
以下内容来自官方blog:https://golang.org/ref/spec#The_zero_value
当通过声明或 new 调用为变量分配存储空间时,或通过复合文字或 make 调用创建新值时,且未提供显式初始化,则给出变量或值一个默认值。此类变量或值的每个元素都为其类型设置为零值:布尔型为 false,数字类型为 0,字符串为 "",指针、函数、接口、切片、通道和映射为 nil。此初始化是递归完成的,例如,如果未指定任何值,则结构体数组的每个元素的字段都将其清零。
例如这两个简单的声明是等价的:
var i int
var i int = 0
在或者这个结构体的声明:
type T struct { i int; f float64; next *T }
t := new(T)
这个结构体t
中成员字段零值如下:
t.i == 0
t.f == 0.0
t.next == nil
Go
语言中这种始终将值设置为已知默认值的特性对于程序的安全性和正确性起到了很重要的作用,这样也使整个Go
程序更简单、更紧凑。
零值有什么用
通过零值来提供默认值
我们在看一些Go
语言库的时候,都会看到在初始化对象时采用"动态初始化"的模式,其实就是在创建对象时判断如果是零值就使用默认值,比如我们在分析hystrix-go
这个库时,在配置Command
时就是使用的这种方式:
func ConfigureCommand(name string, config CommandConfig) {
settingsMutex.Lock()
defer settingsMutex.Unlock()
timeout := DefaultTimeout
if config.Timeout != 0 {
timeout = config.Timeout
}
max := DefaultMaxConcurrent
if config.MaxConcurrentRequests != 0 {
max = config.MaxConcurrentRequests
}
volume := DefaultVolumeThreshold
if config.RequestVolumeThreshold != 0 {
volume = config.RequestVolumeThreshold
}
sleep := DefaultSleepWindow
if config.SleepWindow != 0 {
sleep = config.SleepWindow
}
errorPercent := DefaultErrorPercentThreshold
if config.ErrorPercentThreshold != 0 {
errorPercent = config.ErrorPercentThreshold
}
circuitSettings[name] = &Settings{
Timeout: time.Duration(timeout) * time.Millisecond,
MaxConcurrentRequests: max,
RequestVolumeThreshold: uint64(volume),
SleepWindow: time.Duration(sleep) * time.Millisecond,
ErrorPercentThreshold: errorPercent,
}
}
通过零值判断进行默认值赋值,增强了Go
程序的健壮性。
开箱即用
为什么叫开箱即用呢?因为Go
语言的零值让程序变得更简单了,有些场景我们不需要显示初始化就可以直接用,举几个例子:
-
切片,他的零值是
nil
,即使不用make
进行初始化也是可以直接使用的,例如:
package main
import (
"fmt"
"strings"
)
func main() {
var s []string
s = append(s, "asong")
s = append(s, "真帅")
fmt.Println(strings.Join(s, " "))
}
但是零值也并不是万能的,零值切片不能直接进行赋值操作:
var s []string
s[0] = "asong真帅"
这样的程序就报错了。
-
方法接收者的归纳
利用零值可用的特性,我们配合空结构体的方法接受者特性,可以将方法组合起来,在业务代码中便于后续扩展和维护:
type T struct{}
func (t *T) Run() {
fmt.Println("we run")
}
func main() {
var t T
t.Run()
}
我在一些开源项目中看到很多地方都这样使用了,这样的代码最结构化~。
-
标准库无需显示初始化
我们经常使用sync
包中的mutex
、once
、waitgroup
都是无需显示初始化即可使用,拿mutex
包来举例说明,我们看到mutex
的结构如下:
type Mutex struct {
state int32
sema uint32
}
这两个字段在未显示初始化时默认零值都是0
,所以我们就看到上锁代码就针对这个特性来写的:
func (m *Mutex) Lock() {
// Fast path: grab unlocked mutex.
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
if race.Enabled {
race.Acquire(unsafe.Pointer(m))
}
return
}
// Slow path (outlined so that the fast path can be inlined)
m.lockSlow()
}
原子操作交换时使用的old
值就是0
,这种设计让mutex
调用者无需考虑对mutex
的初始化则可以直接使用。
还有一些其他标准库也使用零值可用的特性,使用方法都一样,就不在举例了。
零值并不是万能
Go
语言零值的设计大大便利了开发者,但是零值并不是万能的,有些场景下零值是不可以直接使用的:
-
未显示初始化的切片、map,他们可以直接操作,但是不能写入数据,否则会引发程序panic:
var s []string
s[0] = "asong"
var m map[string]bool
m["asong"] = true
这两种写法都是错误的使用。
-
零值的指针
零值的指针就是指向nil
的指针,无法直接进行运算,因为是没有无内容的地址:
var p *uint32
*p++ // panic: panic: runtime error: invalid memory address or nil pointer dereference
这样才可以:
func main() {
var p *uint64
a := uint64(0)
p = &a
*p++
fmt.Println(*p) // 1
}
-
零值的error类型
error内置接口类型是表示错误条件的常规接口,nil值表示没有错误,所以调用Error
方法时类型error
不能是零值,否则会引发panic
:
func main() {
rs := res()
fmt.Println(rs.Error())
}
func res() error {
return nil
}
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x10a6f27]
-
闭包中的nil函数
在日常开发中我们会使用到闭包,但是这其中隐藏一个问题,如果我们函数忘记初始化了,那么就会引发panic
:
var f func(a,b,c int)
func main(){
f(1,2,3) // panic: runtime error: invalid memory address or nil pointer dereference
}
-
零值channels
我们都知道channels
的默认值是nil
,给定一个nil channel c
:
-
<-c
从c
接收将永远阻塞 -
c <- v
发送值到c
会永远阻塞 -
close(c)
关闭c
引发panic
关于零值不可用的场景先介绍这些,掌握这些才能在日常开发中减少写bug
的频率。
总结
总结一下本文叙说的几个知识点:
-
Go
语言中所有变量或者值都有默认值,对程序的安全性和正确性起到了很重要的作用 -
Go
语言中的一些标准库利用零值特性来实现,简化操作 -
可以利用"零值可用"的特性可以提升代码的结构化、使代码更简单、更紧凑
-
零值也不是万能的,有一些场景下零值是不可用的,开发时要注