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 |
bool | false |
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 会写入包级变量,请用互斥锁保护它。