Go 变量不赋值时,为什么也有值

0 阅读5分钟

读 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 是布尔类型的一个正常值,通常表示条件不成立。

到这里,我们实际观察了三种类型:

类型零值
int0
string""
boolfalse

“零值”是这类初始值的统称。具体是什么值,要看变量的类型。

可以把 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)。同一个字段名会得到不同的零值,因为决定初始值的是字段的类型。

参考与数据源