深入理解golang中的nil#
nil is (a) zero
什么是零值(zero value)#
1
2
3
4
5
6
7
8
9
10
11
12
| // go中的零值
bool -> false
numbers -> 0
string -> ""
pointers -> nil // point to nothing
slices -> nil // have no backing array
maps -> nil // are not initialized
channels -> nil // are not initialized
functions -> nil // are not initialized
interfaces -> nil // have no value assigned, not even a nil pointer
|
struct中的零值(zero values)#
1
2
3
4
5
6
7
| type Person struct{
Age int
Name string
Friend []Person
}
var p Person // Person{0,"",nil}
|
nil 是什么类型(type)#
“nil is a predeclared identifier in Go that represents zero values for pointers, interfaces, channels, maps, slices and function types.”
nil 是 Go 中预先声明的标识符,表示指针、接口、通道、映射、切片和函数类型的零值。
“nil"的多重特性#
Nil Pointers#
指针是指向内存中某个位置的变量。因此,空指针将不指向任何位置。
Nil Slice#

它有三个属性:一个指向存储实际数据的底层数组的指针、一个表示长度的整型变量,以及另一个表示容量的整型变量。
当你声明一个 nil 切片时,长度(len)和容量(cap)属性仍然存在;只有指向底层数组的指针为 nil。这意味着即使你的切片是 nil,你仍然可以在其上使用以下方法,并且完全有效。
1
2
3
| var s []int
len(s)
cap(s)
|
Nil Interface#
在理解 nil 接口之前,你必须了解接口在内部是如何表示的。因此,它有两个属性:
type 表示接口实现的数据类型(如果提供),value 表示实际值。我们以 fmt.Stringer 接口为例,它只有一个方法 String()。
1
2
| var s fmt.Stringer // (nil, nil)
fmt.Println(s == nil) // true
|
这是因为 nil == nil 成立。
但如果我对这个接口进行一些修改,让它变成这样呢?你认为这个程序的输出会是什么?
1
2
3
4
5
6
7
8
9
10
11
12
| type Person struct{}
func (p Person) String() string {
return ""
}
func main() {
p := Person{}
var s fmt.Stringer = p
fmt.Println(s == nil)
}
|
如果你猜的是 true,那么很抱歉,你错了。因为当我将一个"Person"结构的对象赋值给我们的接口"s"时,其内部布局变成了(Person, nil),正因如此,我们实际上是在比较 Person == nil,这并不成立。这就是为什么这个程序的输出会是"false”。
Nil Maps#
空Map 与空切片类似。你可以对其使用 len() 函数。你也可以遍历空映射,并且可以从空映射中获取值,但返回的将是默认值。唯一不能在空映射上进行的操作是赋值。以下是一段简单的代码来演示这一点。
1
2
3
4
5
6
7
8
9
10
11
12
13
| 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
}
|
假设你有一个生成 HTTP 请求的函数。如果你需要向请求中添加头部信息,你会这样做。
1
2
3
| NewGet("", map[string]string{
"Content-Type": "application/json",
})
|
但如果你不需要任何头部信息,你会这样做。
1
| NewGet("", map[string]string{})
|
有趣的是,即使你向这段代码传递 nil,它依然能够正常运行。
因为我之前提到过,nil 映射并非空无一物。它意味着缺少底层的存储系统,但依然存在其他属性,使我们能够执行一些其他操作,比如迭代。
一些关于nil的实践#
1
2
3
4
5
6
7
| //nil == nil
var s fmt.Stringer // Stringer (nil,nil)
fmt.Println(s == nil) // true
var p *Person // nil of type *Person
var s fmt.Stringer = p // Stringer(*Person,nil)
fmt.Println(s == nil) // false
|
nil not nil ?#
- Do not declare concrete error vars (不要声名错误变量)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| // bad
type NameEmtpyError struct {
name string
}
// NameEmtpyError实现了 Error() 方法的对象
func (e *NameEmtpyError) Error() string {
return "name 不能为空"
}
func do() error {
var err *NameEmtpyError
return err // nil of type *NameEmtpyError
}
func main(){
err := do() // error(*NameEmtpyError,nil)
fmt.Println(err == nil) // false
}
|
1
2
3
4
5
6
7
8
9
10
11
12
| // bad
func do() *NameEmtpyError {
return nil // nil of type *NameEmtpyError
}
func wrapDo() error { // error(*NameEmtpyError, nil)
return do() // nil of type *NameEmtpyError
}
func main(){
err := wrapDo() // error(*NameEmtpyError,nil)
fmt.Println(err == nil) // false
}
|
nil is useful#
1
2
3
4
5
6
| pointers // methods can be callend on nil receivers 方法可以在 nil 接收器上调用
slices // perfectly valid zero values 完全有效的零值映射
maps // perfect as read-only values 完美的只读值
channels // essential for some concurrency patterns 对于某些并发模式必不可少
functions // needed for completeness 完整性所需
interfaces // the most userd signal in Go(err != nil) Go中使用次数最多的信号(err != nil)
|