4.Go 变量和常量

变量与常量:var:=、零值、iota#

背景与动机#

Go 的变量和常量设计看起来很朴素,但它背后有一套很明确的取舍:变量负责“可变的存储位置”,常量负责“编译期可确定的值”,而零值机制则让对象在“未显式初始化”时仍然处于可用状态。

学习这个主题的重点,不是把 var:=const 当成三条语法,而是理解 4 个核心问题:

  1. 变量是怎么声明出来的,类型是显式写还是推导出来。
  2. 为什么 Go 强调零值,而不是强制你先手动初始化。
  3. 常量和变量到底差在哪,为什么常量更“理想化”。
  4. iota 为什么适合定义一组相关常量,而不只是“自增编号”。

核心原理拆解#

1. var 是显式变量声明,核心是“定义一个有类型的存储位置”#

最基础的变量声明方式是 var

var age int
var name string = "Tom"
var enabled = true

它的本质是:声明一个变量,并为这个变量确定静态类型。

var 常见有 3 种形态:

var a int
var b int = 10
var c = 20

分别对应:

  • 只声明类型,不给初值
  • 同时写类型和初值
  • 不写类型,由右值推导类型

这里真正重要的不是写法多,而是语义稳定:

  • 变量一旦声明,类型就确定了
  • 后续赋值必须和这个类型兼容
  • 如果没给初值,就进入该类型的零值

2. := 是短变量声明,核心是“在函数内部边声明边初始化”#

短变量声明语法:

x := 10
name := "Go"
ok, err := true, nil

它等价于“带初始化表达式、但省略类型的局部变量声明”。

但它有两个硬约束:

  1. 只能出现在函数内部
  2. 左边至少要有一个新变量

也就是说,下面是非法的:

x := 1 // 如果写在函数外,非法

而下面这种要特别注意:

x := 1
x := 2 // 非法,没有新变量

但多变量场景下允许“部分重声明”:

a, err := f()
b, err := g() // 合法,b 是新变量,err 是同一代码块里的重用

这里的关键不是“重新定义了 err”,而是:err 并没有新建,只是被重新赋值,真正新声明的是 b

image.png
image.png

3. 零值是 Go 的默认初始化机制,不是语法糖#

Go 规定:变量如果分配了存储但没有显式初始化,就会自动拿到该类型的零值。

典型零值如下:

  • bool -> false
  • 数值类型 -> 0
  • string -> ""
  • 指针、slicemapchanfuncinterface -> nil

例如:

var n int
var s string
var ok bool

它们分别等价于:

var n int = 0
var s string = ""
var ok bool = false

这意味着 Go 把“可安全使用的默认态”放进了语言层,而不是交给程序员手工兜底。

零值的递归性#

零值不只对基本类型成立,对复合类型也递归生效。例如:

type User struct {
Name string
Age int
}

如果写:

var u User

那么:

  • u.Name == ""
  • u.Age == 0

也就是说,结构体的每个字段都会被自动置零。

零值不等于“已经可直接业务使用”#

这点非常重要:

  • nil slice 往往还能安全 append
  • nil map 只能读,不能直接写
  • nil channel 在并发里有特殊阻塞语义

所以零值的含义是“语言级默认状态”,不是“所有场景都能直接完成业务操作”。

4. 常量是编译期值,不是“不可修改的变量”#

Go 用 const 声明常量:

const Pi = 3.14
const MaxUsers = 100

常量和变量的根本区别不是“能不能改”,而是“是否必须在编译期确定”。

这带来几个重要性质:

  • 常量没有运行时存储位置这个核心语义
  • 常量值必须在编译期可确定
  • 许多常量默认是无类型常量(untyped constant)

例如:

const x = 10

这里的 10 在很多上下文里可以自动适配目标类型;而变量不行:

var a int32 = x // 可以

因为 x 是常量,具备更灵活的表示空间。

什么不能做常量#

下面这种不行:

const now = time.Now() // 非法

因为函数调用结果要到运行期才知道,不是编译期常量。

5. iota 是 const 块内的行号生成器,本质是“按 ConstSpec 递增”#

iota 是预声明标识符,只能出现在 const 声明中。它在每个 const 块里从 0 开始,每出现一个新的 ConstSpec 就加 1

最基础的例子:

const (
A = iota // 0
B // 1
C // 2
)

这里省略右侧表达式时,Go 会沿用上一行的表达式,所以 BC 实际上还是在用 iota

更准确地说:

  • iota 在每个 const 块开始时重置为 0
  • 它按“常量声明项”递增,不是按名字个数递增
  • 同一行里多次出现 iota,值相同

例如:

const (
bit0, mask0 = 1<<iota, 1<<iota-1 // iota == 0
bit1, mask1 // iota == 1
_, _ // iota == 2
bit3, mask3 // iota == 3
)

6. iota 最常见的价值不是编号,而是构造“相关常量集”#

iota 的常见用途有两类。

枚举型编号#

const (
StatusPending = iota
StatusRunning
StatusDone
)

适合表达一组顺序相关、语义互斥的状态值。

位标记#

const (
Read = 1 << iota
Write
Execute
)

得到的是:

  • Read = 1
  • Write = 2
  • Execute = 4

这比手写 1, 2, 4 更稳定,因为规则写在表达式里,而不是靠人记忆。

image.png
image.png

最小可运行代码示例#

package main
import "fmt"
const (
StatusPending = iota
StatusRunning
StatusDone
)
func main() {
var count int // 零值为 0
name := "Go" // 短变量声明,只能在函数内使用
const maxRetries = 3 // 编译期常量
fmt.Println(count) // 0
fmt.Println(name) // Go
fmt.Println(maxRetries) // 3
fmt.Println(StatusPending) // 0
fmt.Println(StatusRunning) // 1
fmt.Println(StatusDone) // 2
}

这个例子同时覆盖了:

  • var 的显式声明
  • := 的局部短声明
  • 零值行为
  • constiota 的基本使用

常见陷阱与错误示例#

1. 在函数外使用 :=#

错误示例:

package main
x := 10

:= 只能出现在函数内部,包级变量必须使用 var

2. 误以为 := 永远是“赋值”#

错误示例:

x := 1
x := 2

第二行非法,因为短变量声明要求左边至少有一个新变量。:= 不是普通赋值符号,它是“声明并初始化”。

3. 把零值当成“业务初始化完成”#

错误示例:

var m map[string]int
m["a"] = 1

这会 panic,因为 nil map 不能直接写。零值保证的是语言级默认状态,不保证每种类型都已完成业务初始化。

4. 用运行期值声明常量#

错误示例:

const n = len([]int{1, 2, 3}) // 某些上下文可能成立,但一旦依赖运行期值就不行
const now = time.Now() // 一定不行

常量必须能在编译期确定,不能依赖运行时结果。

5. 误判 iota 的递增规则#

错误理解:

const (
A, B = iota, iota
C, D = iota, iota
)

这里第一行的 AB 值相同,第二行的 CD 值也相同。iota 是按行号增长,不是按同一行里的名字个数增长。

性能影响或设计取舍#

1. 零值降低了初始化成本,但要求你分清“可声明”与“可用”#

Go 的零值设计让大量对象能直接声明后使用,减少样板代码;但代价是你必须理解不同引用类型的零值语义,尤其是 mapchanfunc 和接口值。

2. := 提升局部开发效率,但也容易制造遮蔽问题#

短变量声明让代码更简洁,特别适合局部临时变量;但在 iffor、多返回值接收里,如果名字复用不谨慎,容易引入变量遮蔽和错误作用域判断。

3. 无类型常量让表达更灵活,但也更考验类型边界理解#

Go 常量的一个优势是可以在很多上下文里再决定最终类型,这使得 API 使用更顺滑;代价是初学者容易把“常量可自动适配”误以为“变量也能这样自动兼容”。

4. iota 让相关常量定义更稳定,但不适合过度炫技#

iota 非常适合顺序值和位掩码;但如果表达式写得太花,读者会看不出结果,维护成本会上升。它应该服务于“规则可见”,而不是制造谜语。

面试高频问题#

1. var:= 的区别是什么#

var 可以用于包级和函数级声明,既可以显式写类型,也可以带初始化推导类型;:= 是短变量声明,只能在函数内部使用,并且左边至少要有一个新变量。

2. Go 的零值是什么,为什么重要#

零值是变量在未显式初始化时自动获得的默认值。它让对象在声明后就处于确定状态,减少样板初始化代码,是 Go 设计里“默认可用性”的重要部分。

3. 常量和变量的本质区别是什么#

变量对应可变的存储位置,常量对应编译期可确定的值。常量不是“只读变量”,而是没有同等运行时存储语义的一类值。

4. iota 的递增规则是什么#

iota 只在 const 块里有效,每个 const 块从 0 开始,在每个新的 ConstSpec 处递增一次;同一行里多次使用 iota,值相同。

5. 为什么 nil map 不能写,而零值又说是“可用状态”#

零值保证的是语言级默认初始化,不代表所有类型都已完成业务可写初始化。nil map 可读、可比较是否为 nil,但写入前必须先 make

6. const 为什么不能接收 time.Now() 这类结果#

因为常量必须在编译期确定,而 time.Now() 的结果依赖运行期执行,无法在编译阶段静态确定。

一句话总结#

Go 的变量与常量机制可以概括为:var 定义有类型的存储位置,:= 用于函数内部的简洁局部声明,零值提供统一默认初始化语义,const 表达编译期常量,而 iota 则用来稳定地生成一组相关常量。

文章目录

文章目录