在 Golang 开发中,const 与 var 都用于给标识符绑定值,但两者表达的程序意图并不相同。前者强调固定不变的语义,后者强调可读写、可更新的存储。理解它们的差异,不仅有助于避免编译错误,也能让代码更符合 Go 的表达习惯。围绕定义方式、值可变性、内存表现、类型支持以及工程选择,可以系统把握这两个关键字的边界。

声明语义与初始化规则不同
const 用于声明常量。常量一旦进入程序语义,就代表一个固定值。Go 要求常量在声明时具备明确值,因此通常需要在声明语句中直接完成初始化。即使给常量指定类型,初始值也必须能够在该位置确定。这种约束让常量从源头避免了先占位、后补值带来的不确定性。
var 用于声明变量。变量更像一个可更新的存储位置,可以先声明而不立即赋予业务值,由 Go 赋予对应类型的零值,也可以在声明时初始化。变量声明后可以随着程序执行被重新赋值,因此适合描述运行过程中会发生变化的状态,例如计数、临时结果、配置对象、外部输入等。
从写法上看,两者都很简洁,但背后的约束完全不同。常量更适合表达规则和约定,变量更适合表达过程和状态。下面的示例展示了最基础的差异:
package main
import "fmt"
// 使用 const 定义常量,定义时必须初始化
const pi = 3.1415926
// 使用 var 定义变量,可以先声明后赋值
var appName string
func main() {
appName = "demo"
fmt.Println(pi)
fmt.Println(appName)
// pi = 3.14 会编译失败,因为常量不能被重新赋值
}
在示例中,pi 在声明时就已经确定,后续代码只能读取它,不能重新赋值。appName 则可以先声明为字符串类型,初始阶段是字符串零值,随后在 main 函数中赋值为具体名称。这种差异说明,选择 const 还是 var,首先要回答的问题是,这个值是否在程序语义上保持不变。
值可变性与编译期约束
值可变性是两者最容易被感知的区别。常量不能被重新赋值,任何试图修改常量值的语句都会在编译阶段被拒绝。这种限制并不是运行时检查,而是编译期规则,因此可以更早暴露错误。对于团队协作而言,常量也传递了不要修改的明确信号。
变量则允许在作用域内多次赋值。只要类型匹配,变量可以在不同执行分支中被更新。例如统计数量、请求状态、缓存结果等,都需要变量来承载变化。不过,可变性也意味着代码阅读者需要关注变量在哪些位置被修改,因此变量的作用域和命名应当尽量清晰。
下面的代码通过一个计数场景说明两者的不同:
package main
import "fmt"
const maxCount = 100
var currentCount int
func main() {
currentCount = 10
fmt.Println("初始 currentCount:", currentCount)
currentCount = 20
fmt.Println("修改后的 currentCount:", currentCount)
// maxCount = 200 会编译失败
}
这段代码中,currentCount 可以先写入初始值,再更新为新值,整个过程符合变量语义。若把类似的赋值动作作用到 maxCount 上,编译器会直接报错。由此可见,const 的价值不只是节省内存,更重要的是通过不可变语义减少程序中的隐含修改。
内存分配与可寻址差异
从内存角度看,常量通常不是运行时意义上的存储位置。编译器会在编译阶段知道常量的值,并在合适的位置直接使用该值,或参与常量折叠。因此,常量不具备变量那样的可寻址语义,不能对常量使用取地址操作。
变量则对应运行时的存储。无论最终由编译器如何安排存储位置,变量在语义上都有存储位置,程序可以读取它,也可以获取它的地址。这一特点使变量能够与指针、结构体字段更新、函数参数传递等机制配合。对于需要观察内存地址、构建引用关系或者进行原地修改的场景,变量是必然选择。
下面的示例展示了变量可以取地址,而常量不能取地址:
package main
import "fmt"
const fixedValue = 50
var dynamicValue int = 50
func main() {
fmt.Printf("dynamicValue 的地址: %pn", &dynamicValue)
// fmt.Printf("fixedValue 的地址: %pn", &fixedValue) 会编译失败
}
在示例中,dynamicValue 可以通过取地址操作输出内存地址。若将同样的操作用于 fixedValue,编译会失败。这个差别说明,常量更像编译期可引用的固定值,而变量是运行时可访问的数据容器。理解这一点,有助于解释为什么常量不能承载复杂结构,也不能作为指针来源。
数据类型支持与表达式边界
常量支持的类型范围相对明确。基础数值、字符串、布尔值,以及由这些值组成的编译期常量表达式,都可以使用 const 声明。关键在于,值必须在编译阶段能够确定。只要表达式依赖运行时信息,就不能成为常量。
变量的类型支持则覆盖 Go 的完整类型体系。无论是切片、映射、结构体,还是函数、接口、通道,都可以用 var 声明。引用类型通常需要运行时构造,结构体也可能依赖外部输入,因此它们天然适合变量。即便某些结构体字段在业务上不希望改变,Go 仍然会用变量承载对象本身,再通过设计约束避免随意修改。
下面的示例对比了常量与变量在类型支持上的差别:
package main
import "fmt"
const siteName = "ipipp.com"
const isOpen = true
var scores = []int{90, 85, 88}
type task struct {
name string
done bool
}
var currentTask = task{name: "review", done: false}
func main() {
fmt.Println(siteName)
fmt.Println(isOpen)
fmt.Println(scores)
fmt.Println(currentTask)
}
示例中的 siteName 和 isOpen 都是编译期可以确定的值,因此适合常量。scores 是切片,currentTask 是结构体,它们需要在运行时表示复合数据,因此使用 var 声明。若试图用常量声明切片或结构体,会超出常量表达能力,导致编译错误。
工程实践中的选择原则与常见误区
在实际项目中,选择 const 还是 var,可以遵循不变优先的原则。如果某个值表达的是固定规则、数学常数、协议阈值、错误码文案、开关默认值等,应优先使用常量。这样既能防止误改,也能让读者快速识别稳定配置。如果值会随请求、循环、外部输入或时间变化,则应使用变量。
常见误区主要有两类。第一类是本应固定的值被声明为变量,导致后续代码可能无意修改它,增加排查成本。第二类是把运行时才能得到的值交给常量,例如函数调用结果、网络请求结果、当前时间等,这违背了常量必须在编译期确定的要求。还有开发者误以为常量可以声明任意类型,从而尝试用常量定义切片、映射或结构体,也会遇到编译问题。
下面的示例展示了常见的错误边界:
package main
import (
"fmt"
"time"
)
// const now = time.Now() 会编译失败,因为函数调用结果不是编译期常量
var now = time.Now()
func main() {
const limit = 10
// limit = 20 会编译失败
fmt.Println(limit)
fmt.Println(now)
}
在这段代码中,当前时间来自函数调用,只能使用 var 接收。函数内部的 limit 是局部常量,一旦初始化完成就不能再次赋值。通过这种方式,可以清楚看到常量的边界在哪里,也能避免把运行时结果误当作编译期常量。
核心差异速览
如果将前面的讨论压缩成一张对照表,可以更快建立判断路径。阅读代码时,看到常量应想到固定值、编译期约束和不可重新赋值;看到变量应想到状态、更新和运行时存储。
| 对比角度 | const | var |
|---|---|---|
| 核心语义 | 固定值 | 可变存储 |
| 初始化要求 | 声明时需要确定值 | 可先声明后赋值 |
| 是否可重新赋值 | 不可以 | 可以 |
| 内存表现 | 编译期值引用,不可取地址 | 运行时存储,可取地址 |
| 类型支持 | 基础类型与编译期常量表达式 | 全部 Go 类型 |
| 典型用途 | 固定配置、阈值、错误码 | 状态、结果、复合对象 |
这种对照并不是机械规则,而是帮助我们在设计接口、配置项、循环计数、结构体字段和局部状态时快速做出选择。当业务语义要求稳定时,让编译器协助约束;当业务语义要求变化时,让变量承担状态流转。
总结
总体而言,const 与 var 的区别可以从不变与可变这条主线展开。常量强调编译期确定、不可修改、不可取地址,适合稳定值和固定规则。变量强调运行时存储、可更新、可承载复杂类型,适合描述程序状态。
在实际编码时,先判断值是否稳定,再判断是否需要运行时构造,最后结合可读性和安全性选择声明方式,就能让代码更清晰,也更符合 Go 的语言设计意图。