阅读 2600
Golang 零值、空值与空结构

Golang 零值、空值与空结构

本文首发于 at7h 的个人博客

这篇文章我们讨论下有关 Golang 中的零值(The zero value)、空值(nil)和空结构(The empty struct)的相关问题以及它们的一些用途。

零值

零值是指当你声明变量(分配内存)并未显式初始化时,始终为你的变量自动设置一个默认初始值的策略。

首先我们来看看官方有关零值(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,字符串为 "",数组和结构会递归初始化其元素或字段,即其初始值取决于元素或字段。
  • 对于引用类型: 均为 nil,包括指针 pointer,函数 function,接口 interface,切片 slice,管道 channel,映射 map。

通常,为你声明的变量赋予一个默认值是有用的,尤其是为你数组和结构中的元素或字段设置默认值,这是一种保证安全性和正确性的做法,同时也可以让你的代码保持简洁。

比如,下面的示例是我们常用的,结构体 Value 中包含两个 unexported 字段,sync.Mutex 中也有两个 unexported 字段。因为有默认零值,所以我们可以直接使用:

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()
}
复制代码

因为切片是引用类型的,所以其零值也是 nil

package main

import "fmt"
import "strings"

func main(){
    var s []string

    fmt.Println(s, len(s), cap(s)) // [] 0 0
    fmt.Println(s == nil) // true

    s = append(s, "Hello")
    s = append(s, "World")
    fmt.Println(strings.Join(s, ", ")) // Hello, World
}
复制代码

下面的情况需要特别注意下,有时候不注意就容易混淆,:= 语法糖是声明并且初始化变量的,所以是一个真正的实例(为其分配了内存地址的),并不是零值 nil

package main

import "fmt"
import "reflect"

func main() {
    var s1 []string
    s2 := []string{} // 或者等同于 var s2 = []string{}

    fmt.Println(s1 == nil) // true
    fmt.Println(s2 == nil) // false

    fmt.Println(reflect.DeepEqual(s1, s2)) // false

    fmt.Println(reflect.DeepEqual(s1, []string{}))  // false
    fmt.Println(reflect.DeepEqual(s2, []string{}))  // true
}
复制代码

另外,对于空结构的 nil 是可以调用该类型的方法的,这还可以用来简单地提供默认值:

package main

import "fmt"

const defaultPath = "/usr/bin/"

type Config struct {
    path string
}

func (c *Config) Path() string {
    if c == nil {
            return defaultPath
    }
    return c.path
}

func main() {
    var c1 *Config
    var c2 = &Config{
            path: "/usr/local/bin/",
    }
    fmt.Println(c1.Path(), c2.Path())
}
复制代码

nil

对于一个刚开始使用 Golang 的开发人员,刚开始接触 nil 应该是使用它来检查错误,大致像这样:

func doSomething() error {
    return nil
}

func main(){
    if doSomething() != nil {
        return err
    }
}
复制代码

这是 Golang 惯用的,它鼓励开发人员显式的的将错误作为返回值来处理。现在我们来讨论下这个 nil,在其他语言中也有类似的定义,比如 C、C++、Java 等中的 null,Python 中的 None,但是 Goalng 中的 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 是 Golang 中唯一没有默认类型的非类型化的值,它不是一个未定义的状态。所以你不能像这样使用它:

a := nil
// cannot declare variable as untyped nil: a
复制代码

将一个并没有类型 nil 的值赋给 a 是不对的,编译器不知道它该给 a 分配什么类型。

值得一提的是 Golang 中比较出名的 nil != nil 的问题,我们来看下面的一个例子:

var p *int
var i interface{}

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

fmt.Println(p == i) // false
复制代码

Why?为什么同样都是 nil 却不相等呢?

带着问题,我们再来看一个下面的例子(来自官方 Why is my nil error value not equal to nil):

func Foo() error {
    var err *MyError = nil
    if bad() {
        err = ErrBad
    }
    return err
}

func main() {
    err := Foo()
    fmt.Println(err)        // <nil>
    fmt.Println(err == nil) // false
}
复制代码

其罪魁祸首就是 interface,接口相关的实现原理不在本文的讨论范围,后面再具体分享。其大致原理是,接口要确定一个变量需要两个基础属性:Type and Value,下面我们给上面的两段代码加上注释,就明白了:

var p *int          // (T=*int,V=nil)
var i interface{}   // (T=nil,V=nil)

fmt.Println(p == i) // (T=*int, V=nil) == (T=nil, V=nil) -> false
复制代码
func Foo() error {
    var err *PathError = nil  // (T=*PathError, V=nil)
    if bad() {
        err = ErrBad
    }
    return err  // 这将始终返回 non-nil 错误
}

func main() {
    err := Foo()
    fmt.Println(err)        // <nil>
    fmt.Println(err == nil) // (T=*PathError, V=nil) == (T=nil, V=nil) -> false
}
复制代码

请注意:为了避免此问题,返回错误时请永远使用 error 接口,并且永远不要初始化可能从函数返回的空错误变量。

我们将上面的例子再改改,看下面的例子:

var p *int              // (T=*int, V=nil)
var i interface{}       // (T=nil, V=nil)

fmt.Println(p == nil)   // true
fmt.Println(i == nil)   // true

i = p

fmt.Println(i == nil)     // (T=*int, V=nil) == (T=nil, V=nil) -> false
复制代码

这个问题的实质就是 Go Tour 中的 Interface values with nil underlying values

示例中 i 可以传递给一个 interface{} 作为输入参数的函数,你只检查 i == nil 是不够的。所以对于接口类型的空指针的判断,有些时候你并不能安全的依靠 v == nil,尽管这种检查的坑很少发生,但这有时候可能会使你的程序崩溃。对此,可以有两种方式解决,你可以分别将类型和值分别和 nil 比较或者使用反射包 reflect

请记住:如果接口中已存储任何具体值,那么接口将不会是 nil,详见反射定律

还有就是,也许你也感到困惑,还是上面的例子,为什么下面的类型就可以直接比较并获得准确的结果:

var p *int              // (T=*int, V=nil)

fmt.Println(p == nil)   // true
复制代码

这是因为在进行上面的比较时,因为编译器已经清楚的知道了 p 的类型,所以编译器可以转化为 p == (*int)(nil)。但是对于接口,编译器是没法确定底层类型的,因为它是可以被更改的。

空结构

空结构是没有任何字段的结构类型,例如:

type Q struct{}
var q struct{}
复制代码

既然没有任何字段,那它有什么用呢?

我们知道,一个结构的实例的大小(即所占存储空间的字节数)是由其字段的宽度(size)和对齐(alignment)共同决定的,这样有助于寻址速度,C 语言等都有类似的策略,关于 Golang 的具体策略请阅读 Size and alignment guarantees

很显然,空结构的占用空间大小为零字节:

var q struct {}
fmt.Println(unsafe.Sizeof(q)) // 0
复制代码

由于空结构占用零字节,因此不需要填充对齐,所以由嵌套空结构的空结构也不会占用存储空间。

type Q struct {
        A struct{}
        B struct{
            C struct{}
        }
}
var q Q
fmt.Println(unsafe.Sizeof(q)) // 0
复制代码

由于空结构不占用内存空间,所以我们声明以空结构作为元素的数组或切片,也是不占用空间的(Orthogonality in Go):

var x [1000000000]struct{}
fmt.Println(unsafe.Sizeof(x)) // 0

var y = make([]struct{},1000000000)
fmt.Println(unsafe.Sizeof(x))// 24,背后关联数组为 0
复制代码

对于空结构(或者空数组),其占用的存储大小为零,所以两个不同的零大小的变量在内存中可能具有相同的地址

来看下面几个示例:

var a, b struct{}
fmt.Println(&a == &b)  // true


c := make([]struct{}, 10)
d := make([]struct{}, 20)
fmt.Println(&c[0] == &d[1]) // true


type Q struct{}

func (q *Q)addr() { fmt.Printf("%p\n", q) }

func main() {
        var a, b Q
        a.addr()  // 0x5af5a60
        b.addr()  // 0x5af5a60
}

e := struct{}{} // 不是零值,一个真正的实例
f := struct{}{}
fmt.Println(e == f) // true
复制代码

请注意,这种相等只是可能,并不是一定的。

比如这个示例,相关问题解释请看这个 issue

说了半天,你可能会想,貌似这些也没什么实际的用途啊,下面列举两个比较实用的实践用途:

1. 使用 chan struct{} 代替 chan bool 在 goroutines 之间传递信号。使用 bool 容易让人不理解该值,true or false,但是使用 chan struct{} 就很清楚,我们不在乎值,只关心发生的事儿,更容易表达清楚一些。

2. 为了防止 unkeyed 初始化结构,可以添加 _ struct {} 字段:

type Q struct {
  X, Y int
  _    struct{}
复制代码

这样一来,使用 Q{X: 1, Y: 1} 可以,但使用 Q{1, 1} 就会出现编译错误:too few values in struct initializer,同时这样也帮助了 go ver 代码检查。

参考


同名公众号:

文章分类
后端
文章标签