Go 的变量、常量与零值
Go 的 var、短变量声明 :=、const 与零值语义;包级与函数内声明差异,:= 重声明规则,以及 iota 枚举入口。
[!info] 关联笔记
Go 的变量、常量与零值
这个概念为什么会出现
写出第一个有用的程序前,必须回答:
- 名字如何绑定到可改的存储?
- 未赋值时内存里是什么——随机垃圾,还是确定默认值?
- 哪些量在编译期就固定?
- 函数里如何短促地声明并初始化?
Go 的答案是:变量 + 零值 + 常量三位一体。零值让“忘记初始化”变成可预测状态,而不是 C 系未定义行为;:= 让函数内局部变量声明紧凑;const/iota 承载编译期常量与枚举风格。
[!abstract] 一句话理解 变量用
var或:=声明,未显式赋值时取类型零值;常量用const在编译期固定;:=在同一作用域可对已有变量重声明,但左右至少要有一个新名字。
最小可运行示例
先把示例放进业务场景,再看代码:
场景:订单服务启动时的计数器与配置
电商后台进程启动时要挂几份状态:
- 包级配置常量:服务名在编译期就固定,全进程共用
- 包级计数器:已处理订单数,启动时从 0 开始
- 请求处理函数内的局部变量:当前用户、积分;以及一次“改名 + 错误位”的短声明
零值保证“忘了赋值”时是可预期的空状态,而不是随机垃圾。
package main
import "fmt"
// ServiceName 是进程级配置:编译期固定,不会在运行时被改掉。
// 教学点:const 适合“写死且全进程共享”的名字/版本类配置。
const ServiceName = "order-api"
// processedOrders 是包级可变状态:启动时从零值开始累计。
// 教学点:包级只能用 var,不能用 :=。
var processedOrders = 0
// handleCheckout 模拟一次结账请求里的局部声明。
//
// 业务意图:
// 1. 先拿到“当前用户 / 积分 / 是否就绪”的工作变量;
// 2. 未显式赋值时,零值即可表示“还没填”;
// 3. 用 := 快速声明业务字段,并演示同一作用域的重声明。
func handleCheckout() {
// var 声明:类型明确,初值取零值。
// 业务上:count=待处理商品数,customer=客户名,ready=库存是否齐。
var itemCount int // 零值 0
var customer string // 零值 ""
var ready bool // 零值 false
fmt.Println("zero defaults:", itemCount, customer, ready)
// := 短声明:函数内最常见写法,类型由右侧推导。
customer = "alice" // 先赋值一次,后面演示重声明
customer, score := "alice", 100
fmt.Println("checkout:", customer, score, ServiceName, processedOrders)
// := 重声明规则:左侧至少有一个“新名字”。
// 这里 customer 已存在(重声明),err 是新变量。
// 业务上:把客户名改成最终下单人,并带上一个可忽略的占位 error。
customer, err := "bob", error(nil)
fmt.Println("renamed payer:", customer, err)
// 无类型浮点常量:先以“理想精度”存在,赋给 float64 时再定型。
// 业务上可类比税率、折扣率这类编译期固定小数。
const taxRate = 0.06
var rate float64 = taxRate
fmt.Println("tax rate:", rate)
processedOrders++ // 包级变量在函数内可读写
}
func main() {
handleCheckout()
}
建议运行:
go run .
期望输出:
zero defaults: 0 false
checkout: alice 100 order-api 0
renamed payer: bob <nil>
tax rate: 0.06
结合场景再看三个关注点
-
零值即可用
itemCount/customer/ready不必写= 0/= ""/= false,启动态可预测。 -
:=只在函数内
包级ServiceName、processedOrders用const/var;结账逻辑里才用:=。 -
重声明要有新名字
customer, err := ...能过编译,是因为同时引入了新的err;单独customer := ...在同一作用域会报错。
核心概念与准确模型
变量声明形态
var x int
var y int = 2
var z = 3 // 推导为 int
var a, b int = 1, 2
var (
i int
s string
)
短变量声明:
x := 1
a, b := 1, 2
| 场景 | 用什么 |
|---|---|
| 包级变量 | var(可分组) |
| 函数内短初始化 | := 最常见 |
| 需要指定类型零值或清晰类型 | var x T |
| 不想要零值、立即有值 | := 或 var x = ... |
规范:Var declarations、Short variable declarations。
零值(zero value)
每个类型有零值;变量声明后即有效。
| 类型族 | 零值 |
|---|---|
| 整数 / 浮点 / 复数 | 0 |
bool | false |
string | "" |
| 指针、slice、map、channel、func、interface | nil |
重要:
- 零值是确定语义,不是“未定义”
- 结构体零值=字段各自零值
- 数组零值=元素全零
nil 在不同类型上能否读/写/调用并不相同,见 nil 的多种形态。
参考:The zero value。
:= 与重声明(redeclaration)
短变量声明的特殊规则:
- 必须在函数内
- 左侧非空名字至少有一个在当前作用域是新的
- 已在当前作用域声明过的变量会被重声明(类型必须相同),相当于赋值
- 多变量时,新旧可以混合——这就是
v, err := f()在循环里反复写却不炸的原因
x := 1
x, y := 2, 3 // x 重声明(实为赋值),y 新建
// x, y := 4, 5 // 若 x,y 都已存在且无新变量 → 编译错误
注意作用域:内层块的 := 可能阴影外层变量,而不是重声明。
x := 1
if true {
x := 2 // 新变量,阴影外层
_ = x
}
// 外层 x 仍是 1
常量 const
const Max = 100
const (
A = 1
B = 2
)
const Typed int = 1
要点:
- 常量表达式在编译期求值
- 可表示的类型有限:布尔、字符串、数字(及某些安全的表达式)
- 不能把一般运行期结果(如函数返回的
[]int)标成 const - 无类型常量可在赋值时更灵活地适配多种数值类型(在表示范围内)
iota(简述 + 链接)
const (
Sunday = iota // 0
Monday // 1
Tuesday // 2
)
iota 在 const 块中逐项递增,用于枚举风格常量。完整模式、位掩码、String() 等见 iota 与枚举。
赋值与声明不要混
var x int
x = 1 // 赋值
x := 1 // 若 x 已在当前作用域:非法(无新变量)
空白标识符 _ 可接住不要的值:_, err := f()。
设计动机
- 零值减少未初始化灾难
让 API 可以依赖“零值可用”(如空 struct、nil slice append)。 - 声明语法分层
包级稳定用var/const;局部紧凑用:=。 - 错误处理人体工学
a, err :=/b, err :=重声明规则降低噪音。 - 常量编译期化
配置上限、枚举、位移旗标可安全折叠。
边界情况与反直觉行为
1. 包级不能 :=
// x := 1 // 包级非法
var x = 1
2. 重声明 vs 阴影
同作用域重声明要求类型相同且至少一新变量;内层 := 常是阴影,外层不变。
3. 零值 map 可读不可写
var m map[string]int
_ = m["k"] // 0
// m["k"] = 1 // panic
4. 零值 slice 可 append
var s []int
s = append(s, 1) // ok
5. 无类型常量 vs 变量
const C = 1
var i int32 = C // ok
var j int32 = i // i 是变量,类型已固定
// var k int64 = j // 需要显式转换
常见误区
[!warning] 常见误区:把零值当成未定义或随机值 错误:来自 C 的“局部变量垃圾值”直觉。
正确:Go 变量总有零值。
[!warning] 常见误区:
:=到处用包括包级 错误:包级:=。
正确:包级var/const。
[!warning] 常见误区:以为
:=总是新变量 错误:忽略同作用域重声明与内层阴影。
正确:想改外层变量用=,或在外层先var err error。
[!warning] 常见误区:
nil一视同仁 错误:所有 nil 都能当空容器用。
正确:按类型查 nil 形态。
[!warning] 常见误区:运行期值塞进 const 错误:
const x = time.Now()。
正确:const 仅编译期常量表达式。
与相邻概念对比
| 概念 | 差异 |
|---|---|
| 类型转换 | 改变值的类型视图;声明是绑定名字 |
| 指针 | 变量可取址;指针零值 nil |
| 常量 vs 变量 | 常量不可变、编译期;变量可寻址、可改 |
| iota 枚举 | const 块技巧,不是新存储类 |
工程实践
- 尽量短作用域
在靠近使用处声明;利用if v := ...; ...限制可见范围。 - 错误变量习惯
函数开头可var err error,其后x, err =避免阴影;或有意识使用:=重声明规则。 - 零值可用设计
类型的零值最好有意义(文档写清);避免“必须 New 否则炸”的脆弱 API(除非资源句柄)。 - 包级状态谨慎
可导出包级var有并发与测试隔离成本;配置优先依赖注入。 - 命名
_忽略;不要用:=制造一堆单字母阴影。 - const 块分组
相关常量放一组;枚举用自定义类型 + iota(专题)。 - 测试
断言零值行为(nil slice JSON、空 string)符合 API 契约。
func parse(s string) (int, error) {
if s == "" {
return 0, fmt.Errorf("empty")
}
n, err := strconv.Atoi(s)
if err != nil {
return 0, fmt.Errorf("parse %q: %w", s, err)
}
return n, nil
}
可验证实验
实验 1:打印零值
var 一组基本类型与指针、slice、map,全部打印。
实验 2::= 重声明
a := 1
a, b := 2, 3
// a, b := 4, 5 // 试编译
实验 3:阴影
在 if 块内 x := 修改,块外打印外层 x。
实验 4:包级 :=
在 package 级写 x := 1,阅读错误信息。
实验 5:const 与 iota
写一周枚举,打印数值;尝试 const x = len([]int{1}) 等边界是否合法。
本节总结
- 本质:名字绑定存储(变量)或编译期值(常量);零值保证可预测默认态。
- 关键规则:
var/:=/const分工;零值表;:=至少一新变量的重声明;iota 见专题。 - 最易错:阴影当重声明、包级
:=、nil 一刀切、把零值当未初始化垃圾。 - 下一步:指针、流程控制、iota。
自测题
概念题
- 函数内
var x int与x := 0有何相同与不同? - 短变量声明重声明的“至少一个新变量”是什么意思?
- 为什么说零值是 Go 的设计特性而不是缺陷?
代码推理题
func main() {
x := 1
if x > 0 {
x, err := 2, error(nil)
fmt.Println(x, err)
}
fmt.Println(x)
}
最后打印的 x 是 1 还是 2?为什么?
工程思考题
循环中多次调用返回 (T, error) 的函数,如何避免 err 被内层阴影导致外层检查到错误的变量?
参考答案
展开
- 二者都得到值为 0 的
int;:=必须能推导且仅函数内,var可显式类型、可用于包级、可不写右侧用零值。 - 左侧名字里至少有一个在当前作用域尚未声明,其余已存在者类型相同则重声明(赋值)。
- 它提供确定默认态,支持零值可用 API,避免未初始化随机行为。
代码题:最后是1,因为 if 内x, err :=在内层新作用域阴影了x。
工程题:在外层var err error,循环内用v, err = f();或在每次if v, err := f(); err != nil立刻处理,不把 err 逃逸到错误作用域。
延伸阅读与资料来源
| 资料 | 类型 | 支撑 |
|---|---|---|
| Spec — Variable declarations | 规范 | var |
| Spec — Short variable declarations | 规范 | := 与重声明 |
| Spec — The zero value | 规范 | 零值 |
| Spec — Constant declarations | 规范 | const / iota |
| Tour — Variables | 教程 | 声明入门 |
| Tour — Zero values | 教程 | 零值 |
| Tour — Constants | 教程 | 常量 |
| Effective Go — Constants | 官方文档 | iota 习惯 |