Variables(变量)
变量是带有静态类型、其值可以在运行时改变的存储位置。Go 支持使用 var 在包级或函数内声明变量,也支持在函数内部使用 := 完成短变量声明。理解声明、赋值和作用域的区别,是避免遮蔽和类型错误的关键。
使用 var 声明
var 可以显式指定类型,也可以根据初始值推断类型:
package main
import "fmt"
func main() {
var language string = "Go"
var version = 1
var requests int
fmt.Println(language, version, requests)
}
输出如下:
Go 1 0
没有初始值时,变量获得对应类型的零值。多个相关变量可以一起声明:
var width, height int = 1280, 720
var (
host = "127.0.0.1"
port = 8080
)
变量组适合组织相关的包级状态;函数内部通常优先使用靠近首次使用位置的短小声明。
短变量声明
函数内部可以使用 := 同时声明并初始化变量:
package main
import "fmt"
func main() {
name := "Ada"
count, active := 3, true
fmt.Println(name, count, active)
}
:= 不能用于包级。它也不是普通赋值:在同一作用域中,左侧至少要包含一个新变量,否则编译失败。
count := 1
count, err := loadCount() // err 是当前作用域中的新变量
if err != nil {
return
}
这里 count 被重新赋值,err 被新声明。两个变量必须处于同一作用域;外层已经存在同名变量并不代表内层短声明会复用它。
声明与赋值
声明创建变量,赋值修改已有变量:
var total int // 声明
total = 10 // 赋值
name := "Go" // 短变量声明
name = "Golang"
Go 支持多重赋值,右侧表达式会先求值,再把结果赋给左侧:
left, right := "L", "R"
left, right = right, left
函数返回多个值时,可以一次接收:
value, err := loadValue()
如果某个结果确实不需要,可以使用空白标识符 _ 丢弃,但错误返回值通常不应被无理由忽略。
作用域与生命周期
标识符可见范围由声明位置决定:
- 包级变量可在同一个包的所有文件中访问;首字母大写时还可以被其他包引用。
- 函数参数和返回值名称位于函数作用域内。
- 函数体、
if、for、switch等代码块会形成更小的作用域。 - 内层作用域可以声明与外层同名的变量,此时外层变量被遮蔽。
package main
import "fmt"
func main() {
count := 1
if true {
count := 2
fmt.Println(count) // 2:内层变量
}
fmt.Println(count) // 1:外层变量
}
变量的作用域描述名称在源码中何处可见;生命周期描述对应存储在运行时保留多久。二者相关但不等同。编译器可能把函数内变量分配到堆上,以保证被返回指针等值引用时仍然有效;这属于编译器依据逃逸分析作出的存储决定,不改变语言层面的作用域规则。
避免意外遮蔽
错误处理中的短声明容易产生遮蔽:
var result string
if result, err := loadResult(); err != nil {
return
} else {
fmt.Println(result) // 这里是 if 初始化语句声明的 result
}
fmt.Println(result) // 外层 result 仍是空字符串
如果目的是更新外层变量,应先声明错误变量,再使用普通赋值:
var result string
var err error
result, err = loadResult()
if err != nil {
return
}
缩小变量作用域通常有利于理解代码,但同名变量表达不同含义时会增加误读风险。应优先选择能区分业务含义的名称。
包级变量与初始化
包级变量在包初始化阶段求值,可以由常量、表达式或函数调用初始化:
package config
import "os"
var Environment = os.Getenv("APP_ENV")
包级可变状态会被包内多个调用方共享,可能增加测试隔离和并发同步成本。配置、连接和服务依赖通常更适合由 main 显式创建,再通过参数传入。确实需要共享变量时,应说明所有权、初始化时机和并发访问规则。
赋值会复制值
Go 的赋值会复制变量当前保存的值,但复制后的行为取决于类型内部表示:
package main
import "fmt"
func main() {
a := [2]int{1, 2}
b := a
b[0] = 9
fmt.Println(a, b) // [1 2] [9 2]
x := []int{1, 2}
y := x
y[0] = 9
fmt.Println(x, y) // [9 2] [9 2]
}
数组赋值复制所有元素;切片赋值复制切片描述符,两个切片可能仍引用同一个底层数组。映射、指针、接口和通道也有各自的复制与共享规则,不能简单归纳成“对象按引用传递”。Go 的参数传递同样始终是值传递,只是被复制的值可能包含指向共享数据的引用。
常见错误
- 在包级使用
:=;包级声明必须使用var、const、type或函数声明。 - 在同一作用域中使用
:=,但左侧没有任何新变量。 - 在内层代码块意外声明同名变量,导致外层变量没有被更新。
- 认为声明但未初始化的变量处于“未定义”状态;Go 会赋予零值。
- 假设赋值总会深拷贝,或反过来认为所有值都按引用共享。
- 使用包级可变变量保存请求数据,却没有同步和生命周期边界。
与 TypeScript 的关键差异
- Go 的
var与 TypeScript 的var没有对应关系;Go 不存在 JavaScript 式变量提升语义。 - Go 使用词法块作用域,
:=是声明语法,不是 JavaScript/TypeScript 运算符。 - Go 变量始终有静态类型和零值,不会自然进入
undefined状态。 - Go 参数始终按值传递;切片或映射可能共享底层数据,但这不等同于 JavaScript 对象模型。
涉及类型定义、可赋值性和底层类型时,继续阅读 Type(类型)。
小结
var可用于包级和函数内,:=只能用于函数内部。- 声明创建变量,赋值更新变量;短声明左侧必须至少有一个新变量。
- 代码块形成作用域,内层同名声明会遮蔽外层变量。
- 包级变量会引入共享状态,应明确初始化、所有权和并发规则。
- 赋值始终复制值,但切片等值内部可能仍引用共享数据。