Go语言实践[回顾]教程21--详解Go语言的空值、零值、nil

在很多编程书籍和教程当中,我们经常看到 零值、空值 这类描述,还经常看到 null、nil 这类的值定义。那么在 Go 语言里,零值、空值、nil 又确切的代表什么呢,我们就详细描述一下。

Go 语言中 零值、空值、nil 概念之我见

零值:如果是整数类型,那么零值就很好理解,那就是 0。但是其他类型也都有零值的说法,那该如何定义 零值 的概念呢。针对 Go 语言,可以这样定义,就是基础数据类型变量声明后但没有做初始化时系统默认给设置的那个值。

空值:很多文章书籍将空值描述为等同零值,这点本人在语义上略有不同观点。虽然把零值和空值等同化并不影响代码开发,但是对初学者对深入理解代码经常会造成障碍。个人认为,空值应该是用于描述复合数据类型变量在声明后但没有初始化时的值,也就是这个复合数据类型里面没有任何成员,这个集合是个空的。

nil:Go 语言的 nil 经常会被理解成其他语言中的 null,但是它们是有区别的。在其他大部分语言里,null 代表不存在,拿变量来说就是这个变量根本不存在,没有这个变量。而 Go 语言的 nil 不是这样,它是指这个变量在,但没有任何值(内容),包括零值也没有。又因为 Go 语言在声明基本数据类型的变量时,在没有给定初始化值的情况下会自动初始化为该类型的零值,那么针对数据类型来说,只有复合数据类型才会有 nil 的情况,所以 nil 其实就是空值的意思。

基本数据类型的零值

数据类型零值
数值型(整型、浮点、复数)0
字符串“”(空字符串)
布尔型false

复合数据类型的空值

数据类型零值
slice(切片)nil
map(映射)nil
pointer(指针)nil
channel(通道)nil
func(函数)nil
interface(接口)nil

上表中的 channel、func、interface 后面章节会重点描述,这里先不用去仔细理解。但是大家可能会问,为什么没有数组?因为 Go 语言的数组不支持动态增加元素,而数组的动态操作都是通过切片完成的,所以声明一个没有任何元素的空数组没有实际应用意义。

通过实例体验零值、空值 nil 的差别

// test01 项目的 main 包,文件名 main.go
package main

import (
	"fmt"
)

// 主函数,程序入口
func main() {
	var a int
	var b float32
	var c complex64
	var d string
	var e bool

	var f []int
	var g map[string]int
	var h *int
	var i chan int
	var j func()
	var k interface{}

	fmt.Println("整数型零值:", a)
	fmt.Println("浮点型零值:", b)
	fmt.Println("复数型零值:", c)
	fmt.Println("字符串零值:", d)
	fmt.Println("字符串零值:", e)
	fmt.Println()
	fmt.Println("切片空值:", f, " 是否等于nil:", f == nil)
	fmt.Println("映射空值:", g, " 是否等于nil:", g == nil)
	fmt.Println("指针空值:", h, " 是否等于nil:", h == nil)
	fmt.Println("通道空值:", i, " 是否等于nil:", i == nil)
	fmt.Println("函数空值:", j, " 是否等于nil:", j == nil)
	fmt.Println("接口空值:", k, " 是否等于nil:", k == nil)
}

上述代码编译运行结果如下:

整数型零值: 0
浮点型零值: 0
复数型零值: (0+0i)
字符串零值: 
字符串零值: false

切片空值: []  是否等于nil: true
映射空值: map[]  是否等于nil: true
指针空值: <nil>  是否等于nil: true
通道空值: <nil>  是否等于nil: true
函数空值: <nil>  是否等于nil: true
接口空值: <nil>  是否等于nil: true

从上面编译运行结果可以清楚的看出,零值与空值的区别。基本数据类型的零值就是 0 或 该类型的可以理解为 0 的值,它不能与 nil 进行比较,会报错。而复合数据类型的空值输出也有标志符,但没有任何成员,且与 nil 比较也相等。所以用空值描述复合数据类型未初始化时的值更贴切、易懂,也就是 nil。

nil 的特殊性

▲ nil 不是数据类型,是某个复合类型的空值标识符,所以它也不是一个固定的类型,不同类型的 nil 值变量之间无法比较。所以下面的代码是错误的:

fmt.Println(nil==nil)  // nil 没有固定的类型,只有与声明了空值的复合数据类型变量才可以与 nil 比较

下面的示例也将无法正常运行:

// test01 项目的 main 包,文件名 main.go
package main

import (
	"fmt"
)

// 主函数,程序入口
func main() {
	var a []int
	var b map[string]int

	fmt.Println(a == b)   // 即使都是空值 nil,但因类型不同无法比较
}

上述代码最后一样输出语句会报错,属于不同类型的值在比较,这是不允许的。

下面的示例也将无法正常运行:

// test01 项目的 main 包,文件名 main.go
package main

import (
	"fmt"
)

// 主函数,程序入口
func main() {
	var a []int
	var b []int

	fmt.Println(a == b)   // 即使类型相同,两个空值也无法比较
}

上述代码最后一样输出语句会报错,属于同类型的两个空值在比较,在 Go 语言中,两个空值 nil 的变量是不能进行比较的,这也是与零值的区别。

▲ nil 值的变量是不能直接赋值操作的,因为还没有为其在内存中初始化保存数据的区块。所以直接写入操作会引发错误。如下面的示例无法正产运行:

// test01 项目的 main 包,文件名 main.go
package main

import (
	"fmt"
)

// 主函数,程序入口
func main() {
	var m map[string]int

	m["asong"] = 5 // m 还没有显示声明,所以无法赋值操作

	fmt.Println(m)
}

上述代码无法编译通过,而下面的代码就可以正常编译运行:

// test01 项目的 main 包,文件名 main.go
package main

import (
	"fmt"
)

// 主函数,程序入口
func main() {
	m := make(map[string]int, 8)

	m["asong"] = 5 // m 还没有显示声明,所以无法赋值操作

	fmt.Println(m)
}

后面这个示例可以正常编译运行的原因,是将声明改成了使用 make 函数,make 函数在声明的同时为变量进行了初始化内存,所以可以正常赋值操作了。这里也看出了使用 make 声明的与直接声明的区别,以及零值空值有很多不同。

自动赋零值的意义

为了提高程序的安全性、正确性、书写效率,简化编程者操作,Go 语言提供了基础数据类型默认零值的特性。同时零值是可以使用的,是代码可以更简单、紧凑,无需显示初始化,开箱即用。零值还可以作为判断依据,来识别变量的赋值状态。
.
.
上一节:Go语言实践[回顾]教程20–详解Go语言复合数据类型之映射 map

下一节:Go语言实践[回顾]教程22–详解Go语言的流程控制
.


版权声明:本文为yyykj原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。