读 Go 代码时,可能会遇到这样一行:
var count int
它给变量取了名字,也写明了整数类型,却没有用等号提供初始值。接下来直接读取 count,会报错,还是得到某个值?
先运行一个小程序,观察实际结果。
没写初始值,为什么会输出 0
在已安装 Go 的电脑上,新建 main.go,写入:
package main
import "fmt"
func main() {
var count int
fmt.Println(count)
}
package main 表示这里的代码属于可执行程序的入口包;import "fmt" 引入标准库中的输出工具。程序执行时会进入 main 函数,运行大括号里的语句。fmt.Println(count) 把变量的值输出到屏幕,并换行。
在这个文件所在目录运行:
go run main.go
结果是:
0
关键就在 var count int:var 用来声明变量,count 是变量名,int 是它的类型,表示整数。
Go 规定,没有提供显式初始值的变量,会得到其类型对应的零值。 int 的零值是 0,所以这行声明完成后,count 已经有了确定的值,可以直接读取。Go 规范中的零值
因此,“这行没有写初始值”和“这个变量没有任何值”是两回事。
声明、初始化和赋值有什么关系
把 main 函数改成下面这样,文件顶部的 package 和 import 保留:
func main() {
var count int
fmt.Println(count)
count = 5
fmt.Println(count)
}
再次运行,输出:
0
5
沿着执行顺序,可以区分三个概念:
- 声明:引入一个变量,确定它的名字和类型。这里用的是
var count int。 - 初始化:让新变量获得初始值。这行声明没有显式提供初始值,因此
count得到零值 0。 - 赋值:把一个值存入变量。后面的
count = 5将已有变量的值改为 5。
声明和初始化不是必须写成两行的操作。var count int 这一行就同时完成了它们。
也可以在声明时明确提供初始值。把那一行换成:
var count int = 5
变量一开始就是 5。这里的 5 来自代码中明确写出的初始值;int 的零值仍然是 0,没有因此改变。
零值不一定是数字 0
再观察字符串和布尔值。字符串用来保存文字,类型名是 string;布尔值只有 true 和 false 两个值,类型名是 bool。
将 main 函数改为:
func main() {
var label string
var ready bool
fmt.Printf("label: %q\n", label)
fmt.Println("ready:", ready)
}
这里用到了另一种输出函数 fmt.Printf。它根据第一个参数里的格式输出后面的值:%q 会给字符串加上引号,\n 表示换行。这样即使字符串没有内容,也能看出它确实被输出了。
运行结果:
label: ""
ready: false
"" 是空字符串:里面一个字符也没有。它与 " " 不同,后者包含一个空格。false 是布尔类型的一个正常值,通常表示条件不成立。
到这里,我们实际观察了三种类型:
| 类型 | 零值 |
|---|---|
int | 0 |
string | "" |
bool | false |
“零值”是这类初始值的统称。具体是什么值,要看变量的类型。
可以把 var label string 改成 var label string = "Go" 再运行。输出会变成 label: "Go",而 ready 仍然是 false:给一个变量提供初始值,不会改变另一个变量的初始化规则。
结构体会怎样初始化
如果把刚才的三个值放到一起呢?
Go 可以用结构体把多个字段组合成一种类型。字段可以理解为这组数据中各自有名字、有类型的成员:
type Sample struct {
Count int
Label string
Ready bool
}
type 用来定义类型,Sample 是我们起的类型名,struct 表示这是结构体。大括号里依次列出了三个字段及其类型。
把这段定义放在 main 函数前面,再将 main 改成:
func main() {
var sample Sample
fmt.Println("sample.Count:", sample.Count)
fmt.Printf("sample.Label: %q\n", sample.Label)
fmt.Println("sample.Ready:", sample.Ready)
}
var sample Sample 与之前的 var count int 形式相同,只是这次变量的类型换成了 Sample。sample.Count 中的点号用于访问结构体的字段。
输出:
sample.Count: 0
sample.Label: ""
sample.Ready: false
结构体的各个字段,分别得到了自身类型的零值。我们没有逐个给字段赋值,它们也都具有确定的初始状态。这也解释了为什么读 Go 代码时,常会看到结构体变量声明后就被使用。
值为 0,能说明之前没有赋过值吗
再做一个比较。把 main 改成:
func main() {
var automatic int
var explicit int = 0
fmt.Println(automatic == explicit)
}
automatic 没有显式初始值,得到零值 0;explicit 明确以 0 作为初始值。== 用于比较两边的值是否相等,相等时结果是 true。
运行结果:
true
两个整数的值相同。单看一个普通 int 的当前值,无法判断它是自动初始化为 0,还是代码明确写入了 0。 这个整数本身没有额外保存赋值历史。
零值让变量具有确定的起点,减少了“使用前先写一个 0”这样的重复操作。但它也意味着,漏写一次原本需要的赋值,未必会报错:程序可能继续使用 0,而这个值并不符合原本的意图。
读到 var count int 时,可以确定它初始为 0;至于这个 0 是否符合程序的需要,还要结合后面的代码判断。
将这些观察放进同一个程序
下面是完整版本。保存为 main.go,运行 go run main.go,就能一起观察前面的结果:
package main
import "fmt"
type Sample struct {
Count int
Label string
Ready bool
}
func main() {
var count int
fmt.Println("count:", count)
count = 5
fmt.Println("count after assignment:", count)
var label string
var ready bool
fmt.Printf("label: %q\n", label)
fmt.Println("ready:", ready)
var sample Sample
fmt.Println("sample.Count:", sample.Count)
fmt.Printf("sample.Label: %q\n", sample.Label)
fmt.Println("sample.Ready:", sample.Ready)
var automatic int
var explicit int = 0
fmt.Println("same value:", automatic == explicit)
}
输出:
count: 0
count after assignment: 5
label: ""
ready: false
sample.Count: 0
sample.Label: ""
sample.Ready: false
same value: true
试着把 Sample 中的 Count int 改成 Count string,并将它的输出改为 fmt.Printf("sample.Count: %q\n", sample.Count)。同一个字段名会得到不同的零值,因为决定初始值的是字段的类型。