Golang在变量声明的设计上和C、Java这类传统语言差别很大,类型写在变量名之后不说,还额外提供了短变量声明这种极简语法。不少刚接触Go的开发者会困惑:什么时候该用var,什么时候该用:=,常量又为什么可以不指定类型直接参与各种运算。这篇文章把Go语言里变量与常量的定义方式完整梳理一遍,从基本语法到作用域规则,再到容易踩坑的细节,配合可直接运行的代码示例,帮助读者彻底掌握这部分基础但关键的内容。

一、var关键字:最基础的变量声明方式
var是Go语言中最正统的声明关键字,完整语法是var 变量名 类型 = 初始值。和多数主流语言最大的不同在于类型的位置:Go把类型放在变量名后面,这种设计让声明语句读起来更接近自然语言,例如var age int可以顺口读成声明一个int类型的age。初学者刚开始会不习惯,写几段代码之后反而会觉得这种顺序更清晰。
var支持几种简化写法。第一种是省略初始值,此时变量会被自动赋予该类型的零值,比如int是0、string是空字符串;第二种是省略类型,由编译器根据右侧表达式自动推断;第三种是借助括号批量声明一组变量,这在声明数据库连接配置、日志参数这类成组的变量时非常实用,可以避免反复书写var关键字。
package main
import "fmt"
// 包级别变量只能使用 var 声明
var globalCount int = 100
// 省略类型,编译器推断为 string
var appName = "order-service"
// 批量声明多个变量
var (
host string
port int
debug bool
)
func main() {
// 只声明不初始化,age 的值为 0
var age int
fmt.Println(age) // 0
// 声明并初始化
var name string = "gopher"
fmt.Println(name)
// 类型推断,score 自动识别为 float64
var score = 95.5
fmt.Printf("类型是 %T\n", score)
}
需要特别注意的一点是:在函数外部也就是包级别声明变量时,只能使用var,短变量声明在包级别属于非法语法。另外,Go不允许声明了一个变量却完全不使用它,哪怕只是赋值过一次也算未使用,编译器会直接报错。这条强约束逼着开发者及时清理无用代码,是Go保持工程整洁的手段之一。
二、短变量声明:=的适用场景与限制
短变量声明:=是Go提供的语法糖,写法是变量名 := 表达式,它同时完成声明和初始化,类型完全交给编译器推断。这种写法比var简洁得多,实际开发中函数内部的局部变量绝大多数都用它声明。不过它有一条硬性限制:只能在函数内部使用,包级别的变量声明如果用了冒号等号,编译器会直接报语法错误。
短变量声明还有一条容易被忽略的规则:当它和已有变量一起出现在左侧时,左侧必须至少有一个是新变量,否则编译不通过。这个特性常用于接收函数的多个返回值,比如val, err := strconv.Atoi(s),即使err之前已经声明过,只要val是新变量,这行代码依然合法,此时err会被重新赋值而不是重新声明。
package main
import (
"fmt"
"strconv"
)
func main() {
// 函数内部使用短变量声明
count := 10
message := "hello"
// 多变量同时声明,类型可以不同
x, y := 1, "two"
fmt.Println(count, message, x, y)
var err error
// val 是新变量,err 是已声明变量,这种写法合法
val, err := strconv.Atoi("42")
fmt.Println(val, err)
}
短变量声明最容易埋雷的地方是变量遮蔽。在内部作用域用:=声明一个和外层同名的变量时,Go不会报错,而是创建一个全新的局部变量,外层变量的值保持不变。这类问题往往在if、for语句块里悄悄发生,程序运行结果和预期不符却很难一眼看出原因。可以借助go vet或者一些静态检查工具来发现潜在的遮蔽问题。
package main
import "fmt"
var shadow = "包级变量"
func main() {
fmt.Println(shadow) // 包级变量
// 在新作用域中创建同名局部变量,外层不受影响
shadow := "局部变量"
fmt.Println(shadow) // 局部变量
}
三、用const定义常量:编译期就定死的值
常量使用const关键字声明,它的值在编译期就必须确定,之后永远不能修改。常量的声明语法和var类似,同样支持省略类型和批量声明。由于常量在编译期就被求值并直接写进指令,它没有内存地址,也就不能对它取地址,这一点和C语言的宏、Java的static final常量在思路上是相通的。
Go常量有一个非常独特的特性:无类型常量。如果一个常量声明时没有指定类型,它就属于无类型常量,拥有比变量大得多的自由度,同一个无类型常量可以按需适配不同的类型上下文。比如声明const num = 10之后,num既可以当作int用,也可以当作float64、int64用,编译器会在使用处自动完成适配。一旦显式指定了类型,这种灵活性就消失了,赋值给其他类型会直接报编译错误。
package main
import "fmt"
const (
Pi = 3.14159
AppName = "demo"
)
const (
// 无类型常量,使用时自动适配上下文
untypedNum = 10
// 显式指定类型
typedNum int64 = 20
)
func main() {
var a int = untypedNum
var b float64 = untypedNum
fmt.Println(a, b) // 10 10
// typedNum 已经是 int64,赋给 int 会编译报错
// var c int = typedNum
// 常量不可修改,下面这行会报错
// Pi = 3.14
}
还有一个细节值得留意:常量的类型推断和变量不同。var x = 10推断出的是int,而const y = 10得到的是一个无类型整数常量,它不是int,而是一个可以随时变成int、int64、float64的潜在类型值。理解了这一点,就能明白为什么常量在数值运算中很少出现类型不匹配的问题。
四、iota:Go语言实现枚举的标准姿势
Go没有enum关键字,实现枚举的标准做法是const配合iota。iota是const声明块里的自增计数器,从0开始,const块中每出现一行新的常量声明,iota就加1。它在编译期展开,可以参与任意表达式运算,灵活性远超简单的自增。
package main
import "fmt"
// iota 从 0 开始自动递增
const (
Sunday = iota // 0
Monday // 1
Tuesday // 2
Wednesday // 3
)
// 利用 iota 和位运算定义权限标志位
const (
Read = 1 << iota // 1
Write // 2
Execute // 4
)
// 跳过某些值也是常见技巧
const (
_ = iota // 忽略 0
KB = 1 << (10 * iota) // 1 << 10
MB // 1 << 20
GB // 1 << 30
)
func main() {
fmt.Println(Sunday, Monday, Tuesday, Wednesday)
fmt.Println(Read, Write, Execute)
fmt.Println(KB, MB, GB)
}
上面第三组声明是文件大小常量的经典写法,借助iota配合移位运算,一行就能表达1KB等于2的10次方这样的含义,比手写1048576直观太多。使用iota时要注意它只在const块里生效,且重复表达式会在后续行自动沿用,这正是Monday到Wednesday不需要重复写等号iota的原因。
五、零值机制与作用域规则
Go坚持任何声明的变量一定有值,这个值就是该类型的零值。零值机制意味着开发者不需要为了初始化而初始化,一个刚声明的map、切片或者结构体指针,行为是完全可预期的。各类型的零值如下表所示:
| 类型 | 零值 |
|---|---|
| int、float64等数值类型 | 0 |
| bool | false |
| string | 空字符串 |
| 指针、切片、map、channel、函数 | nil |
| 数组 | 各元素均为对应类型的零值 |
作用域方面,Go遵循词法作用域,变量从声明处开始,到所在代码块结束为止有效。包级别变量在整个包内可见,首字母大写的还会导出给外部包使用;函数内声明的变量只在函数及其嵌套块中可见。内层代码块可以声明与外层同名的变量,这就是前面提到的遮蔽现象,写代码时应当有意识地避免在嵌套块里复用外层变量名。
package main
import "fmt"
func main() {
var (
i int
f float64
s string
b bool
p *int
)
// 打印各类型的零值
fmt.Println(i, f, s, b, p) // 0 0 "" false nil
// 声明但未使用的变量会直接编译报错
// unused := "declared and not used"
}
最后总结一下选择建议:包级别变量用var,函数内优先用:=,需要显式控制类型或者只声明暂不赋值时用var;固定不变的值一律用const,枚举和成组的标志位交给iota。把这些规则内化成习惯,写出来的Go代码会明显更加地道,也能避开大量低级编译错误。

