GO [ 变量 ]

前面我们已经学习了 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"

编译器会分别推断出 intfloat64string

一次声明多个变量

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, a

Go 会先把等号右边所有表达式全部算完,再一次性给左边变量赋值。

不是一步步:先把 ba,再拿新 ab

初始:a=1b=2

  1. 第一步:先算右边全部的值 右边:b 的值是 2a 的值是 1 → 把右侧结果暂存成一组:(2, 1)

    ⚠️ 这一步不会修改 a、b,只是读取当前值

  2. 第二步:同时赋值给左边 左边第一个变量 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 使用词法作用域。变量在哪个代码块声明,就根据代码块规则决定它能被哪些位置访问。

常见作用域包括:

  • 包级变量:同一个包中的文件可以按规则访问;
  • 函数参数和返回值:作用域在函数体内;
  • 函数内部变量:作用域通常从声明位置开始,到所在代码块结束;
  • ifforswitch 内的变量:只在对应语句的代码块中有效。
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,而不是 intnew(int) 适合需要一个指向零值变量的指针时使用,但普通代码不需要为了"申请内存"而滥用 new

newmake 的作用不同:

函数 作用 返回值
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 不会自动混合 intint32float64 等不同数值类型。需要运算时,应明确写出转换,并确认转换不会造成溢出或精度损失。

误区五:把 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 变量的完整使用方式:

  1. 变量是运行时保存值的存储位置,类型决定变量可以保存什么;
  2. var 可以用于显式类型、零值声明、类型推断和分组声明;
  3. := 只能在函数内部使用,并且左侧必须至少有一个新变量;
  4. 没有显式初始值的变量会自动获得零值;
  5. 多重赋值可以交换变量,也可以接收函数的多个返回值;
  6. _ 可以丢弃暂时不需要的返回值,但不应该随意忽略错误;
  7. := 和变量作用域结合时容易产生遮蔽问题;
  8. 指针变量保存地址,new 返回指向零值变量的指针,make 用于初始化切片、映射和通道。

真正理解 Go 变量之后,就不会把 var:=、零值和指针混为一谈。更准确的理解是:变量是类型约束下的运行时存储位置;声明方式决定变量何时、在哪里产生,作用域决定谁能访问它,而零值让很多 Go 类型在没有构造函数的情况下也能安全开始工作。

官方资料:

相关推荐
lpfasd1231 小时前
展厅中控总体架构
go·串口·中控
茉莉玫瑰花茶5 小时前
GO [ 条件/循环控制 ]
go
茉莉玫瑰花茶6 小时前
GO [ 数据类型 ]
go
云浪1 天前
Go Context 到底是什么?
后端·go
newerp1 天前
系统调用与 M/P 解绑
后端·程序员·go
newerp1 天前
抢占式调度实现细节
后端·程序员·go
小小龙学IT1 天前
Go 语言 net/http 网络编程深度解析:从 Handler 到生产级 HTTP 服务
go
名字还没想好☜1 天前
Go 实现指数退避重试:context 取消、抖动 jitter 与什么时候别重试
后端·golang·go
我不会起名字3221 天前
一天一道力扣Hot100(37):深度优先算法--括号生成
java·数据结构·c++·后端·python·算法·go