Skip to content

02|变量与常量

程序需要存储和操作数据。变量是一块命名的内存空间,里面的值在运行时可以改变;常量是一块在编译期就确定的值,运行过程中不能修改。Go 的变量声明有多种方式,每种适用的场景不同。理解这些差异,能避免在包级别、函数级别和块级别作用域里出现命名冲突或意外覆盖。

一、var 声明

var 是 Go 最基本的变量声明方式,可以在包级别或函数级别使用:

go
package main

import "fmt"

var appName = "go-gateway" // 包级别变量

func main() {
	var port int = 8080
	var enabled bool = true

	fmt.Println(appName, port, enabled)
}

var 声明时,类型可以显式写在等号后面,也可以让编译器根据右侧的值自动推断:

go
var port int = 8080     // 显式类型
var host = "0.0.0.0"    // 编译器推断为 string

多个同类型变量可以合并声明:

go
var host, port = "0.0.0.0", 8080

包级别的 var 变量在程序启动时初始化,初始化顺序按它们在代码中出现的顺序执行。如果包 A 依赖包 B 的变量,包 B 的变量会先初始化。

二、短变量声明 :=

函数内部最常用的是 :=,编译器根据右侧的值自动推断类型,不需要写 var 和类型名:

go
func main() {
	port := 8080        // int
	host := "0.0.0.0"   // string
	ratio := 0.75       // float64
	active := true      // bool

	fmt.Println(port, host, ratio, active)
}

:= 只能在函数内部使用,包级别声明必须用 var:= 还有一个限制:左侧至少有一个变量是首次在当前作用域声明。如果左侧所有变量都已经在当前作用域存在,编译会报错:

go
port := 8080
port := 9090 // 编译错误:no new variables on left side of :=

重新赋值应该用 =

go
port := 8080
port = 9090 // 正确

如果 := 左侧既有新变量也有已存在的变量,已存在的变量会被重新赋值,新变量会被声明。这种"声明+赋值"混合的写法在错误处理里很常见:

go
result, err := doSomething() // 新声明 result 和 err
if err != nil {
	return err
}

other, err := doAnother() // other 是新声明,err 是重新赋值
if err != nil {
	return err
}

三、作用域

变量的可见范围由它声明的位置决定。

包级别作用域:在函数外面用 var 声明的变量,整个包内的文件都能访问。首字母大写的变量还能被其他包导入后访问:

go
package config

var DefaultTimeout = 30 // 其他包可以通过 config.DefaultTimeout 访问
var defaultLogLevel = "info" // 只在 config 包内可见

函数级别作用域:在函数内部用 var:= 声明的变量,只在函数内部可见。参数和返回值也属于函数级别作用域。

块级别作用域:在 ifforswitch 的代码块里声明的变量,只在该块内部可见:

go
func checkPort(host string) {
	if host == "" {
		host = "127.0.0.1" // 这个 host 和参数 host 是同一个变量
	}
	fmt.Println(host)
}

func checkWithLocal() {
	if err := connect(); err != nil { // err 只在 if 块内可见
		fmt.Println(err)
	}
	// err 在这里不可见
}

短语句里的变量有更小的作用域,这能避免错误变量被误用到后面的代码里。

四、零值

变量声明后如果没有显式赋值,会自动获得该类型的零值。零值不是未定义,也不是 nil

类型零值说明
string""空字符串
int / int640数字零
float640.0
boolfalse
指针、接口、切片、映射、通道、函数nil未指向任何对象

零值是 Go 的一个安全设计。变量始终有确定的初始值,不会因为忘记初始化而读到随机数据。但这也有隐患——布尔标志忘了赋值时默认是 false,计数器默认是 0,如果业务逻辑里把它们当成"未配置"处理,就会误判。

五、常量

不会改变的值用 const 声明。常量在编译期确定,不能赋值为运行时才有的结果(比如函数返回值、变量值):

go
const defaultTimeout = 30
const appName = "go-gateway"

常量可以没有显式类型,此时它是无类型常量,可以自动匹配多种类型:

go
const maxRetries = 3

var retries int = maxRetries      // 匹配为 int
var scale float64 = maxRetries    // 也能匹配为 float64

需要限制常量类型时,显式声明:

go
const maxTimeout int = 30

多个常量可以批量声明:

go
const (
	StatusOK       = 0
	StatusWarning  = 1
	StatusCritical = 2
)

六、iota

iota 用于生成连续的常量值,从 0 开始自动递增。它只在 const 块内有效:

go
const (
	StatusOK = iota      // 0
	StatusWarning        // 1
	StatusCritical       // 2
)

iota 常用于枚举状态码,减少手写数字的出错概率。插入新状态时,后续值会自动后移:

go
const (
	StatusOK = iota      // 0
	StatusWarning        // 1
	StatusUnknown        // 新增,值为 2
	StatusCritical       // 3
)

iota 也可以配合位移运算生成位掩码:

go
const (
	Read = 1 << iota   // 1
	Write              // 2
	Execute            // 4
)

七、常见错误

包级别用 :=

go
package main

app := "go-gateway" // 编译错误:unexpected := at top level

包级别必须用 var

未使用变量导致编译失败

go
func main() {
	var unused string = "hello" // 编译错误:unused 未使用
	fmt.Println("ok")
}

Go 编译器拒绝编译存在未使用变量的程序。这个规则比 Python、JavaScript 严格,但能减少"声明了但忘了用"的脏代码。如果变量暂时不用,用 _ 接收丢弃的值:

go
result, _ := someFunction() // 忽略第二个返回值

:= 在同作用域重复声明

go
port := 80
port := "8080" // 编译错误:port 已经声明为 int

同名变量必须保持同一类型。:= 不等于"重新声明",在同一作用域里只能声明一次。

短语句变量作用域混淆

go
func main() {
	err := doSomething()
	if err != nil {
		result, err := doAnother() // 这里的 err 是新的局部变量
		fmt.Println(result)
	}
	// 外层的 err 没有被重新赋值
}

if 块内的 := 声明了新的 err,外层的 err 不受影响。如果意图是让外层 err 被更新,不能用 :=

go
func main() {
	err := doSomething()
	if err != nil {
		var result string
		result, err = doAnother() // 重新赋值,不声明新变量
		fmt.Println(result)
	}
	// 外层的 err 被更新了
}