← [ BACK_TO_NOTES ]

Go 的变量、常量与零值

Go 的 var、短变量声明 :=、const 与零值语义;包级与函数内声明差异,:= 重声明规则,以及 iota 枚举入口。

#concept #growing #dev #go

[!info] Related notes

Go 的变量、常量与零值

这个概念为什么会出现

写出第一个有用的程序前,必须回答:

  • 名字如何绑定到可改的存储?
  • 未赋值时内存里是什么——随机垃圾,还是确定默认值?
  • 哪些量在编译期就固定?
  • 函数里如何短促地声明并初始化?

Go 的答案是:变量 + 零值 + 常量三位一体。零值让“忘记初始化”变成可预测状态,而不是 C 系未定义行为;:= 让函数内局部变量声明紧凑;const/iota 承载编译期常量与枚举风格。

[!abstract] 一句话理解 变量用 var 或 := 声明,未显式赋值时取类型零值;常量用 const 在编译期固定;:= 在同一作用域可对已有变量重声明,但左侧至少要有一个非 _ 的新变量。

最小可运行示例

先把示例放进业务场景,再看代码:

场景:订单服务启动时的计数器与配置

电商后台进程启动时要挂几份状态:

  1. 包级配置常量:服务名在编译期就固定,全进程共用
  2. 包级计数器:已处理订单数,启动时从 0 开始
  3. 请求处理函数内的局部变量:当前用户、积分;以及一次“改名 + 错误位”的短声明

零值保证“忘了赋值”时是可预期的空状态,而不是随机垃圾。

package main

import "fmt"

// ServiceName 是进程级配置:编译期固定,不会在运行时被改掉。
// 教学点:const 适合“写死且全进程共享”的名字/版本类配置。
const ServiceName = "order-api"

// processedOrders 是包级可变状态:启动时从零值开始累计。
// 教学点:包级只能用 var,不能用 :=。
var processedOrders int

// 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(nil) 是显式类型的 nil,不代表真实错误处理。
	customer, err := "bob", error(nil)
	fmt.Println("renamed payer:", customer, err)

	// 无类型浮点常量:先以“理想精度”存在,赋给 float64 时再定型。
	// 转为 float64 后按二进制浮点表示,不代表十进制金额可精确计算。
	const taxRate = 0.06
	var rate float64 = taxRate
	fmt.Println("tax rate:", rate)

	processedOrders++ // 本例仅顺序调用;并发调用需同步此共享状态
}

func main() {
	handleCheckout()
}

保存为 main.go 后运行:

go run main.go

期望输出:

zero defaults: 0  false
checkout: alice 100 order-api 0
renamed payer: bob <nil>
tax rate: 0.06

结合场景再看三个关注点

  1. 零值即可用
    itemCount/customer/ready 不必写 = 0 / = "" / = false,启动态可预测。

  2. := 只在函数内
    包级 ServiceName、processedOrders 用 const/var;结账逻辑里才用 :=。

  3. 重声明要有新名字
    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
boolfalse
string""
指针、slice、map、channel、func、interfacenil

重要:

  • 零值是确定语义,不是“未定义”
  • 结构体零值=字段各自零值
  • 数组零值=每个元素各自类型的零值

nil 在不同类型上能否读/写/调用并不相同,见 nil 的多种形态。

参考:The zero value。

:= 与重声明(redeclaration)

短变量声明的特殊规则:

  1. 必须在函数内
  2. 左侧非 _ 名字至少有一个在当前块是新的;左侧非 _ 名字不能重复
  3. 同一块已有变量可被重声明(类型不变),相当于赋值;函数体顶层还可重声明参数/命名返回值
  4. 多变量时可混合新旧名字;同一块再写一次相同的 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 声明中从 0 开始的 ConstSpec 序号,每个声明项递增一次;A, B = iota, iota 中两次 iota 相同。省略表达式会重复前一声明项的表达式和类型,新 const 声明重新从 0 开始。完整模式、位掩码、String() 等见 iota 与枚举。

赋值与声明不要混

var x int
x = 1     // 赋值
x := 1    // 若 x 已在当前作用域:非法(无新变量)

空白标识符 _ 可接住不要的值:_, err := f() 仅在 err 是当前块的新变量时可用;若 err 已存在应写 _, err = f()。_ 不算新变量。

设计动机

  1. 零值减少未初始化灾难
    让 API 可以依赖“零值可用”(如空 struct、nil slice append)。
  2. 声明语法分层
    包级稳定用 var/const;局部紧凑用 :=。
  3. 错误处理人体工学
    a, err := / b, err := 重声明规则降低噪音。
  4. 常量编译期化
    配置上限、枚举、位移旗标可安全折叠。

边界情况与反直觉行为

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 的“局部变量垃圾值”直觉。
正确:没有初始化表达式的变量取得类型零值;显式初始化则取表达式的值。

[!warning] 常见误区::= 到处用包括包级 错误:包级 :=。
正确:包级 var/const。

[!warning] 常见误区:以为 := 总是新变量 错误:忽略同作用域重声明与内层阴影。
正确:想改外层变量用 =;多返回值赋值前先声明所有目标,单独提前声明 err 不能阻止内层 := 遮蔽它。

[!warning] 常见误区:nil 一视同仁 错误:所有 nil 都能当空容器用。
正确:按类型查 nil 形态。

[!warning] 常见误区:运行期值塞进 const 错误:const x = time.Now()。
正确:const 仅编译期常量表达式。

与相邻概念对比

概念差异
类型转换改变值的类型视图;声明是绑定名字
指针变量可取址;指针零值 nil
常量 vs 变量常量不可变、编译期;变量可寻址、可改
iota 枚举const 块技巧,不是新存储类

工程实践

  1. 尽量短作用域
    在靠近使用处声明;利用 if v := ...; ... 限制可见范围。
  2. 错误变量习惯
    若要保留外层结果,先声明所有赋值目标(包括 x 与 err),其后 x, err = 避免阴影;或有意识使用 := 重声明规则。
  3. 零值可用设计
    类型的零值最好有意义(文档写清);避免“必须 New 否则炸”的脆弱 API(除非资源句柄)。
  4. 包级状态谨慎
    可导出包级 var 有并发与测试隔离成本;配置优先依赖注入。
  5. 命名
    _ 忽略;不要用 := 制造一堆单字母阴影。
  6. const 块分组
    相关常量放一组;枚举用自定义类型 + iota(专题)。
  7. 测试
    断言零值行为(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}) 非法;const x = len([1]int{1}) 合法。数组/数组指针的 len 是否为常量,还要求其表达式不含 channel 接收或非常量函数调用。

验证记录(Go 1.26.8):最小示例输出、参数重声明、内层遮蔽、iota 和数组 len 已运行核对;另验证了 _ 不算新变量、重复左侧名字、nil 推导、slice len 常量、重复声明及类型变化的编译失败。

本节总结

  • 本质:名字绑定存储(变量)或编译期值(常量);零值保证可预测默认态。
  • 关键规则:var/:=/const 分工;零值表;:= 至少一新变量的重声明;iota 见专题。
  • 最易错:阴影当重声明、包级 :=、nil 一刀切、把零值当未初始化垃圾。
  • 下一步:指针、流程控制、iota。

自测题

概念题

  1. 函数内 var x int 与 x := 0 有何相同与不同?
  2. 短变量声明重声明的“至少一个新变量”是什么意思?
  3. 为什么说零值是 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 被内层阴影导致外层检查到错误的变量?

参考答案

展开
  1. 二者都得到值为 0 的 int;:= 必须能推导且仅函数内,var 可显式类型、可用于包级、可不写右侧用零值。
  2. 左侧至少一个非 _ 新变量;同一块已有变量保持原类型并被赋值,左侧非 _ 名字必须唯一。
  3. 它提供确定默认态,支持零值可用 API,避免未初始化随机行为。
    代码题:最后是 1,因为 if 内 x, err := 在内层新作用域阴影了 x。
    工程题:在外层声明 v 与 err,循环内用 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 习惯

笔记元信息

  • 建议文件名:go-variables-constants-and-zero-values.md
  • 所属阶段:语言基础起点
  • 学习顺序:基本类型之后
  • 建议下一篇:流程控制 或 指针
  • 本篇状态:已深化(iota 仅简述并链接专题)
创建于 2026/6/20 更新于 2026/10/7
Digital Biome
Access

键盘导航

j / k / ↑ / ↓
选择对象
Enter / o
打开选择
/ 或 Ctrl+K
全站搜索
i / f / 1–4
筛选当前列表
g → h / l / p / t / a
首页 / 笔记 / 项目 / 工具 / About
r → 1–9
打开所选对象的关联
u
返回上一页
Esc
关闭浮层 / 取消命令;输入框内清空文本