
前面我们已经学习了 Go 的基本语法、数据类型以及常量。接下来开始学习 Go 语言中最常用的基础概念:变量。
很多初学者会把变量理解成"一个可以改变的值"。这个理解没错,但还不够完整。按照 Go 官方语言规范 的定义,变量是一个用于保存值的存储位置,变量允许保存的值由它的类型决定。
简单理解:变量就是程序运行过程中用来保存数据的存储位置,变量的类型决定它能保存什么值,变量的作用域决定代码在哪些地方可以访问它。
变量和常量的核心区别是:常量在编译期确定,变量在运行期保存状态并且可以重新赋值。
本章按照"官方定义 → 声明方式 → 实际代码 → 运行结果 → 原因解释"的顺序,把 Go 变量真正讲清楚。我们重点学习:
var的多种声明方式;- 短变量声明
:=的使用边界; - 变量的零值和初始化;
- 变量的类型推断与显式类型;
- 多变量声明和多重赋值;
- 变量作用域、遮蔽和空白标识符;
- 指针变量、
new以及复合类型变量的基本行为。
变量的基本声明
Go 使用 var 关键字声明变量。最完整的写法是:
Go
var name string = "rose"
这句话可以拆成三部分理解:
var:声明变量的关键字;name:变量名称;string:变量的静态类型;"rose":变量的初始值。
我们编写一个最简单的例子:
Go
package main
import "fmt"
func main() {
var name string = "rose"
var age int = 18
fmt.Println("name:", name)
fmt.Println("age:", age)
}
运行结果:
Go
name: rose
age: 18
变量声明完成后,可以通过变量名读取值,也可以重新赋值:
Go
var age int = 18
age = 19
fmt.Println(age)
运行结果:
Go
19
这里的 age = 19 不是重新声明变量,而是给已经存在的变量写入新值。重新赋值时不能再次使用 var:
Go
var age int = 18
// var age int = 19 // 编译错误:age redeclared in this block
age = 19 // 正确:给已有变量赋值
var 的几种常见写法
Go 允许根据实际场景选择不同的变量声明形式。
只声明变量,不提供初始值
Go
var count int
var message string
var passed bool
这种写法不会产生未初始化的随机数据。Go 会自动为变量设置对应类型的零值,后面会专门讲解。
声明时提供类型和值
Go
var count int = 10
var price float64 = 19.9
这种写法最明确,适合希望直接表达变量类型的场景。
声明时省略类型
如果右侧有初始值,可以让编译器根据右侧表达式推断类型:
Go
var count = 10
var price = 19.9
var message = "hello"
编译器会分别推断出 int、float64 和 string。
一次声明多个变量
Go
var width, height int = 800, 600
var first, second = "Go", "Rust"
同一条声明中的变量数量必须和初始化表达式数量对应。
使用括号进行分组声明
当多个变量属于同一组时,可以使用括号集中声明:
Go
var (
appName = "go-demo"
appVersion = "1.0.0"
maxRetry = 3
)
分组声明常用于包级变量,也适合把同一功能模块的配置集中在一起。
变量的零值
Go 官方规范规定:如果变量声明时没有提供初始值,Go 会自动为它设置零值。零值不是随机值,也不是"没有初始化",而是该类型规定的默认值。
| 类型 | 零值 |
|---|---|
| 布尔类型 | false |
| 整数类型 | 0 |
| 浮点类型 | 0 |
| 复数类型 | 0 + 0i |
| 字符串类型 | "" |
| 指针 | nil |
| 函数 | nil |
| 接口 | nil |
| 切片 | nil |
| 映射 | nil |
| 通道 | nil |
zero_value.go
Go
package main
import "fmt"
func main() {
var passed bool
var count int
var price float64
var message string
var numbers []int
var scores map[string]int
var pointer *int
fmt.Printf("bool: %v\n", passed)
fmt.Printf("int: %d\n", count)
fmt.Printf("float: %g\n", price)
fmt.Printf("string: %q\n", message)
fmt.Printf("slice: %v\n", numbers)
fmt.Printf("map: %v\n", scores)
fmt.Printf("pointer: %v\n", pointer)
}
运行结果:
Go
bool: false
int: 0
float: 0
string: ""
slice: []
map: map[]
pointer: <nil>
打印出来的 nil 切片和 nil 映射看起来像空集合,但它们和已经分配空间的空切片、空映射并不是一回事:
Go
var nilSlice []int
emptySlice := []int{}
fmt.Println(nilSlice == nil) // true
fmt.Println(emptySlice == nil) // false
切片的零值可以直接使用 append,映射的零值却不能直接写入:
Go
var numbers []int
numbers = append(numbers, 1) // 合法
var scores map[string]int
// scores["Go"] = 100 // 运行时 panic:assignment to entry in nil map
scores = make(map[string]int)
scores["Go"] = 100 // 合法
这就是 Go "零值可用"设计中的一个重要边界:不同类型的零值能力不同,使用前仍然要理解类型本身的规则。
类型推断
变量声明时如果没有显式写类型,编译器会根据初始化表达式推断变量类型。
type_inference.go
Go
package main
import "fmt"
func main() {
var age = 18
var price = 19.9
var name = "rose"
var passed = true
fmt.Printf("age: %T\n", age)
fmt.Printf("price: %T\n", price)
fmt.Printf("name: %T\n", name)
fmt.Printf("passed: %T\n", passed)
}
运行结果:
Go
age: int
price: float64
name: string
passed: bool
这里的类型一旦确定,就不能把其他不兼容类型的值直接赋给变量:
Go
var age = 18
// age = "18" // 编译错误:cannot use "18" as int value
类型推断只是省略了书写,不是取消了类型。Go 仍然会为每个变量确定明确的静态类型。
短变量声明 :=
在函数内部,Go 提供了更简洁的短变量声明:
Go
name := "rose"
age := 18
它等价于:
Go
var name = "rose"
var age = 18
短变量声明只能出现在函数内部,不能在包级作用域直接使用:
Go
// name := "rose" // 编译错误:non-declaration statement outside function body
:= 的左侧必须至少声明一个新变量:
Go
name := "rose"
name = "jasmine" // 正确,这是赋值
// name := "tea" // 编译错误:no new variables on left side of :=
:= 的变量重声明规则
短变量声明允许在同一个作用域内重新使用已经存在的变量,但前提是左侧至少还有一个新变量:
Go
package main
import "fmt"
func main() {
name, age := "rose", 18
name, city := "jasmine", "Xi'an"
fmt.Println(name, age, city)
}
运行结果:
bash
jasmine 18 Xi'an
第二行中,name 已经存在,所以它被重新赋值;city 是新变量,因此整条 := 声明合法。
如果左侧没有任何新变量,就会编译失败:
Go
name, age := "rose", 18
// name, age := "jasmine", 20 // 编译错误:no new variables on left side of :=
:= 不能替代所有 var
下面这些场景应该使用 var:
- 包级变量声明;
- 需要显式指定变量类型;
- 只声明变量、暂时使用零值;
- 希望代码明确表达"这里是在声明变量"。
例如:
Go
var buffer []byte
var timeout time.Duration = 3 * time.Second
而函数内部已经有初始值时,短变量声明通常更简洁:
Go
func main() {
buffer := make([]byte, 1024)
_ = buffer
}
多变量声明和多重赋值
Go 支持在一条语句中声明或赋值多个变量:
Go
var a, b int = 1, 2
a, b = b, a
fmt.Println(a, b)
运行结果:
Go
2 1
交换过程中不需要额外的临时变量。Go 会先计算右侧表达式,再同时完成左侧赋值。
a, b = b, aGo 会先把等号右边所有表达式全部算完,再一次性给左边变量赋值。
不是一步步:先把
b给a,再拿新a给b!
初始:a=1,b=2
-
第一步:先算右边全部的值 右边:
b的值是2,a的值是1→ 把右侧结果暂存成一组:(2, 1)⚠️ 这一步不会修改 a、b,只是读取当前值
-
第二步:同时赋值给左边 左边第一个变量
a接收第一个暂存值2左边第二个变量b接收第二个暂存值1
✅ 最后 a=2,b=1,交换成功。
函数返回多个值
多重赋值最常见的用途,是接收返回多个结果的函数:
Go
package main
import "fmt"
func divide(a, b int) (int, bool) {
if b == 0 {
return 0, false
}
return a / b, true
}
func main() {
result, ok := divide(10, 2)
fmt.Println(result, ok)
}
运行结果:
Go
5 true
Go 经常把实际结果和错误信息一起返回:
Go
value, err := loadConfig()
if err != nil {
return err
}
fmt.Println(value)
这种写法把"结果"和"是否成功"明确分开,是 Go 错误处理风格的重要基础。【核心重点】
空白标识符 _
如果多返回值中有一个结果暂时不需要,可以使用空白标识符 _ 丢弃它:
Go
result, _ := divide(10, 2)
fmt.Println(result)
在 range 循环中也很常见:
Go
names := []string{"Go", "Rust", "C++"}
for _, name := range names {
fmt.Println(name)
}
这里不需要切片下标,因此用 _ 丢弃第一个返回值。
需要特别注意:空白标识符只是"丢弃结果",并不会把错误变成安全状态。下面这种写法可能隐藏真实问题:
Go
// data, _ := os.ReadFile("config.json") // 不建议忽略 error
如果函数返回了 error,通常应该检查它,而不是直接丢弃。
变量的作用域
Go 使用词法作用域。变量在哪个代码块声明,就根据代码块规则决定它能被哪些位置访问。
常见作用域包括:
- 包级变量:同一个包中的文件可以按规则访问;
- 函数参数和返回值:作用域在函数体内;
- 函数内部变量:作用域通常从声明位置开始,到所在代码块结束;
if、for、switch内的变量:只在对应语句的代码块中有效。
Go
package main
import "fmt"
var packageName = "main"
func main() {
functionName := "main"
if true {
blockName := "if-block"
fmt.Println(packageName, functionName, blockName)
}
// fmt.Println(blockName) // 编译错误:blockName 只在 if 代码块内有效
}
变量遮蔽
内层代码块可以声明一个和外层同名的变量。内层变量会暂时遮蔽外层变量:
Go
package main
import "fmt"
func main() {
name := "outer"
if true {
name := "inner"
fmt.Println("代码块内部:", name)
}
fmt.Println("代码块外部:", name)
}
运行结果:
Go
代码块内部: inner
代码块外部: outer
这里内层的 name := 创建了一个新变量,并没有修改外层的 name。如果想修改外层变量,应该使用赋值运算符:
Go
name := "outer"
if true {
name = "inner"
}
fmt.Println(name) // inner
变量遮蔽在处理 err、配置值和循环变量时很容易造成误判。遇到"明明赋值了但外层值没变化"的问题,优先检查是否误用了 :=。
包级变量和函数内变量
包级变量写在函数外面,生命周期通常覆盖整个程序运行过程:
Go
package main
import "fmt"
var version = "1.0.0"
func main() {
fmt.Println(version)
}
函数内部变量只在函数执行期间使用:
Go
func main() {
version := "1.0.0"
fmt.Println(version)
}
包级变量适合表达真正的包级状态或配置,但不要为了省事把所有数据都定义成全局变量。全局变量会增加隐式依赖,使函数难以测试,也容易产生并发读写问题。
如果一个值只在函数内使用,优先定义为局部变量;如果多个函数需要共享状态,应该明确设计访问方式,而不是无条件扩大变量作用域。
变量和指针
变量保存的是值,指针变量保存的是另一个变量的地址。使用 & 可以获取变量地址,使用 * 可以访问指针指向的值。
pointer.go
Go
package main
import "fmt"
func main() {
value := 10
pointer := &value
fmt.Println("value:", value)
fmt.Println("*pointer:", *pointer)
*pointer = 20
fmt.Println("value after update:", value)
}
运行结果:
Go
value: 10
*pointer: 10
value after update: 20
pointer 没有保存一份独立的整数,而是指向 value 的存储位置,所以通过 *pointer = 20 修改后,读取 value 也会得到 20。
指针变量本身也有零值:
Go
var pointer *int
fmt.Println(pointer == nil) // true
不能直接对 nil 指针解引用:
Go
var pointer *int
// fmt.Println(*pointer) // 运行时 panic:nil pointer dereference
使用前需要先判断是否为 nil,或者为它指向一个有效变量。
new 创建变量
内置函数 new(T) 会分配一个类型为 T 的变量,并返回它的地址 *T。新变量的内容是该类型的零值。
Go
package main
import "fmt"
func main() {
count := new(int)
fmt.Println(*count)
*count = 100
fmt.Println(*count)
}
运行结果:
bash
0 100
这里 count 的类型是 *int,而不是 int。new(int) 适合需要一个指向零值变量的指针时使用,但普通代码不需要为了"申请内存"而滥用 new。
new 和 make 的作用不同:
| 函数 | 作用 | 返回值 |
|---|---|---|
new(T) |
分配一个 T 类型的零值变量 |
*T |
make(T, ...) |
初始化切片、映射或通道 | T |
例如:
Go
pointer := new(int) // *int
numbers := make([]int, 3) // []int
scores := make(map[string]int) // map[string]int
messages := make(chan string, 1) // chan string
_, _, _, _ = pointer, numbers, scores, messages
结构体中的变量
结构体变量由多个字段组成,每个字段都可以单独读写。没有显式初始化的字段会自动使用零值。
Go
package main
import "fmt"
type User struct {
Name string
Age int
Active bool
}
func main() {
var user User
user.Name = "rose"
fmt.Printf("%+v\n", user)
}
运行结果:
Go
{Name:rose Age:0 Active:false}
结构体的零值是所有字段都为零值的结构体。只设置需要的字段,其他字段会自动保持零值:
Go
user := User{Name: "rose"}
fmt.Printf("%+v\n", user)
这种"零值可用"的设计可以减少大量构造函数代码,也是 Go 类型设计中非常重要的习惯。
变量初始化顺序
包级变量会在 main 函数执行前初始化。初始化顺序会根据变量之间的依赖关系决定,而不是简单按照我们阅读代码时的直觉决定。
Go:包级变量(全局变量)初始化,会自动分析依赖关系,按依赖顺序初始化,在main()之前全部做完
C++:全局变量初始化顺序在不同编译单元 (.cpp) 之间是未定义行为;同文件内按代码书写顺序,不能自动解析跨变量依赖
Go
package main
import "fmt"
var first = second + 1
var second = 2
func main() {
fmt.Println(first, second)
}
运行结果:
Go
3 2
first 依赖 second,所以 Go 会先完成 second 的初始化,再计算 first。实际项目中不建议把复杂逻辑塞进包级变量初始化表达式;如果初始化过程有错误处理、外部依赖或明显副作用,通常应该放到显式函数中。
变量声明中的常见错误
误区一:在函数外使用 :=
Go
// port := 8080 // 编译错误
var port = 8080 // 包级变量应该使用 var
误区二:以为 := 一定会创建新变量
Go
name := "outer"
if true {
name := "inner" // 创建了新的内层变量,遮蔽外层 name
fmt.Println(name)
}
需要修改外层变量时使用 =,不要使用 :=。
误区三:变量声明后不使用
Go 编译器会把函数内部声明但未使用的变量视为错误:
Go
func main() {
name := "rose"
// 编译错误:declared and not used: name
_ = name // 临时消除错误,但正式代码应真正使用它
}
未使用的包级变量通常不会触发同样的错误,但这不代表应该随意保留无用全局状态。
误区四:把不同类型的变量直接混用
Go
var count int = 10
var size int32 = 20
// total := count + size // 编译错误:mismatched types int and int32
total := count + int(size)
fmt.Println(total)
Go 不会自动混合 int、int32、float64 等不同数值类型。需要运算时,应明确写出转换,并确认转换不会造成溢出或精度损失。
误区五:把 nil 切片、nil 映射和空集合混为一谈
Go
var nilSlice []int
emptySlice := []int{}
fmt.Println(nilSlice == nil) // true
fmt.Println(emptySlice == nil) // false
对于映射,读取 nil 映射是安全的,但写入会 panic:
Go
var scores map[string]int
fmt.Println(scores["Go"]) // 读取得到 0
// scores["Go"] = 100 // 写入会 panic
一份可以直接复制的变量示例
最后把本章几个重点合并到一个小程序中:使用零值、短变量声明、指针、结构体和多重赋值。
variables_demo.go
Go
package main
import "fmt"
type User struct {
Name string
Age int
Active bool
}
func swap(a, b int) (int, int) {
return b, a
}
func main() {
var user User
user.Name = "rose"
age := 18
pointer := &age
*pointer = 19
left, right := swap(1, 2)
fmt.Printf("user: %+v\n", user)
fmt.Println("age:", age)
fmt.Println("swapped:", left, right)
}
运行结果:
Go
user: {Name:rose Age:0 Active:false}
age: 19
swapped: 2 1
这里可以看到:
var user User先创建一个结构体零值;user.Name = "rose"只修改其中一个字段;age := 18使用短变量声明;pointer := &age获取变量地址;*pointer = 19通过指针修改原变量;left, right := swap(1, 2)使用多重赋值接收多个返回值。
总结
本章我们学习了 Go 变量的完整使用方式:
- 变量是运行时保存值的存储位置,类型决定变量可以保存什么;
var可以用于显式类型、零值声明、类型推断和分组声明;:=只能在函数内部使用,并且左侧必须至少有一个新变量;- 没有显式初始值的变量会自动获得零值;
- 多重赋值可以交换变量,也可以接收函数的多个返回值;
_可以丢弃暂时不需要的返回值,但不应该随意忽略错误;:=和变量作用域结合时容易产生遮蔽问题;- 指针变量保存地址,
new返回指向零值变量的指针,make用于初始化切片、映射和通道。
真正理解 Go 变量之后,就不会把 var、:=、零值和指针混为一谈。更准确的理解是:变量是类型约束下的运行时存储位置;声明方式决定变量何时、在哪里产生,作用域决定谁能访问它,而零值让很多 Go 类型在没有构造函数的情况下也能安全开始工作。
官方资料: