Menu

Golang 变量:var、:=、零值与变量遮蔽

如何在 Go 中用 var 和简短形式 := 声明变量,什么是零值,多重赋值,以及常让 Go 新手栽跟头的变量遮蔽和未使用变量错误。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

Go 有两种声明变量的方式:在任何地方都能用的 var,以及只能在函数内部使用、会推断类型的简短形式 :=。

每个变量都有一个在编译时就确定的固定类型。给出值时,Go 会根据值推断类型,所以再写一遍类型(var city string = "Lisbon")虽然合法,但是多余。

var 与 :=

var:=
位置包级别和函数内部只能在函数内部
类型可选;没有值时必须写总是推断
初始值可选(否则为零值)必须有
重复声明不允许只要左边至少有一个新变量就允许

在函数内部,地道的写法是:

  • 有初始值时用 x := value。
  • 想要零值时用 var x T,这表示“有意留空”:var buf bytes.Buffer、var total int。
  • 推断出的类型不是你想要的类型时,用 var x T = value,例如 var ratio float64 = 1(直接写 ratio := 1 得到的是 int)。

在函数外部使用 := 是语法错误:

./main.go:5:1: syntax error: non-declaration statement outside function body

零值

声明时没有赋值的变量绝不会是垃圾值。它保存着其类型的零值:

类型零值
整数、浮点数0
boolfalse
string""
指针、切片、map、通道、函数、接口nil
数组、结构体每个元素或字段都设为各自的零值

好的 Go API 会把零值设计得可以直接使用:零值的 sync.Mutex 处于未加锁状态,零值的 bytes.Buffer 是空的、可以直接用,len、range 和 append 都能作用于 nil 切片。需要注意的是 map:读取 nil map 会返回零值,但向它写入会 panic。存入数据之前,先用 make 或字面量创建 map。

声明多个变量

var ( ... ) 块把相关的包级变量放在一起。在 x, y = y, x 这样的多重赋值中,右边的所有表达式都会在赋值之前求值,所以交换才能成功。

只要左边至少有一个名字是新的,多名字的 := 就不会重复声明任何东西。同一作用域中已存在的名字只是被赋值。正因如此,你才能复用 err:

f, err := os.Open("a.txt")   // declares f and err
g, err := os.Open("b.txt")   // declares g, assigns to the existing err

如果没有一个名字是新的,就会报错:

./main.go:7:4: no new variables on left side of :=

给已经存在的变量赋值,请用普通的 =。

未使用的变量是错误

声明了却从未被读取的局部变量会导致构建失败:

./main.go:9:2: declared and not used: x

这不是一个可以关掉的警告。删除这个变量、使用它,或者在调试时用空白标识符让它安静下来:_ = x。空白标识符也可以丢弃你不需要的返回值:_, err := fmt.Println("hi")。

包级变量和函数参数不受这条规则限制。

作用域与变量遮蔽

变量从声明处开始存在,直到所在代码块({ ... })结束。内层代码块中的 := 会创建一个新变量,即使外面已经有同名变量。外层的变量只是被遮蔽,而没有被修改:

程序先打印 inside: 42,再打印 outside: 0。if 内部的 := 在内层代码块中把 count 和 err 都声明成了新变量,因为它们在那里都还不存在。这段代码可以顺利编译,是“我的值不见了”这类 bug 的经典来源。

解决办法是先声明 err,再用 = 赋值:

var err error
count, err = strconv.Atoi(input)

go vet 默认不会报告变量遮蔽。单独的 shadow 分析器会报告,golangci-lint 等检查工具可以启用它。

命名

  • 使用 camelCase:maxRetries、userID。不要用 max_retries。
  • 缩写词保持同一种大小写:userID、httpClient、parseURL,而不是 userId。
  • 作用域短就用短名字:循环里用 i,reader 用 r,错误用 err。包级变量用更长、更有描述性的名字。
  • 包级别首字母大写的名字(var Timeout)会被导出给其他包。参见包与导入一节。

包级变量

在函数外声明的变量在整个程序运行期间都存在,并在 main 运行之前按依赖顺序初始化。它们适合用来存放配置和缓存,而且被所有 goroutine 共享。如果有不止一个 goroutine 写入同一个包级变量,请用互斥锁保护它,或者使用 sync/atomic,否则就会出现数据竞争。

对于永远不会改变的值,优先使用常量。const 不会被意外修改,在运行时也没有任何开销。

常见问题

Go 中 var 和 := 有什么区别?

:= 声明并初始化一个变量,类型由推断得出,只能在函数内部使用:count := 10。var 在任何地方都能用(包括包级别),可以写明类型,也可以省略初始值而得到零值:var count int。在函数内部,地道的 Go 写法是有初始值时用 :=,想要零值时用 var。

Go 中的零值是什么?

每个声明时没有赋值的变量都会得到其类型的零值:数字是 0,bool 是 false,string 是 "",指针、切片、map、通道、函数和接口是 nil。结构体的每个字段都取各自的零值。Go 中不存在未初始化的变量。

Go 中如何解决“declared and not used”错误?

Go 不允许一个被赋值却从未被读取的局部变量。使用它、删除它,或者在调试时用 _ = x 把它赋给空白标识符。包级变量和函数参数不受这条规则限制。

Go 中如何声明全局变量?

在包级别、所有函数之外使用 var:var counter int。那里不允许使用简短形式 :=。首字母大写的名字(var Counter int)对其他包可见;小写的名字只在自己的包内可见。如果有 goroutine 会写入包级变量,请用互斥锁保护它。

Coddy 编程语言插图

和 Coddy 一起学编程

开始