目标
了解Go 的变量声明有以下几个重要特点:
- 声明方式多样
- 使用
var
关键字可以显式声明变量、指定类型或初始化值。 - 使用简短声明符号
:=
可以在函数内部快速声明并初始化变量,类型由编译器自动推导。
- 使用
- 零值初始化
- 如果在声明时没有提供初始化表达式,变量会被赋予类型对应的"零值":数值为 0,布尔为 false,字符串为空字符串,指针、slice、map、chan、函数等为 nil。
- 多变量声明与多重赋值
- 同一个声明语句中可以同时声明多个变量,甚至允许类型各不相同(通过初始化表达式推导)。
- 还可以利用函数返回值进行多变量的声明和初始化。
- 变量与指针
- 每个变量对应一块内存区域,指针变量存储其他变量的地址,通过指针可以间接读取或修改目标变量的值。
- 变量的生命周期与作用域
- 包级变量在整个程序运行期间存在,而局部变量仅在所在函数(或块)内有效。
- 编译器通过"逃逸分析"决定局部变量是分配在栈上还是堆上,从而影响性能和内存回收。
概念
-
零值
- 之所以需要零值初始化:每个变量声明后都有一个"合理"的初始状态,避免出现未初始化的情况,从而减少潜在的错误
- 各类型的零值 :
- 数值类型:0
- 布尔类型:false
- 字符串:""
- 指针、slice、map、chan、函数、接口:nil
- 数组、结构体等聚合类型:每个元素或字段都被初始化为对应类型的零值
-
内存管理
-
变量和内存:
每个变量在内存中都有一个地址。变量声明时分配内存空间,指针保存该内存地址,可以直接操作内存中的数据。
-
栈与堆:
- 栈:用于分配局部变量,速度快但空间有限(因此分配和回收速度快)
- 堆:当变量需要在函数外部保持有效时,可能会分配在堆上,由垃圾回收器管理。
-
-
变量的生命周期与作用域
变量的生命周期决定了它们在内存中存在的时间长短,而作用域决定了变量在代码中可以被访问的范围。
- 包级变量 :
- 在包级(文件最外层)声明的变量,其生命周期与整个程序运行周期相同。
- 可以在同一包的所有文件中访问(如果名字符合可见性规则)。
- 局部变量 :
- 在函数或代码块中声明的变量,其生命周期仅限于所在的作用域。
- 当变量不再被引用时,自动成为垃圾回收的对象。
- 逃逸分析 :
- 如果局部变量的地址被引用到外部(例如赋值给包级变量),则称该变量"逃逸"到堆上,其生命周期会延长。
- 编译器会自动决定局部变量分配在栈上还是堆上,但逃逸变量可能会影响性能,因为堆分配和垃圾回收开销较大。
- 垃圾回收:Go 的垃圾收集器通过从全局变量和当前活动函数的局部变量出发,跟踪所有可达变量来决定哪些变量可以回收。只要变量不再被引用,它们的内存就会被自动释放。
- 从每个包级的变量和每个当前运行函数的每一个局部变量开始,通过指针或引用的访问路径遍历,是否可以找到该变量。如果不存在这样的访问路径,那么说明该变量是不可达的,也就是说它是否存在并不会影响程序后续的计算结果。
- 因为一个变量的有效周期只取决于是否可达,因此一个循环迭代内部的局部变量的生命周期可能超出其局部作用域。同时,局部变量可能在函数返回之后依然存在。
- 包级变量 :
要点
基本的var声明
go
var 变量名字 类型 = 表达式
- 类型或表达式可省略
- 如果省略类型,则根据初始化表达式推导类型;
- 如果省略初始化表达式,则使用该类型的零值来初始化变量。
同时声明多个变量
go
var i, j, k int // 三个变量都是 int 类型
var b, f, s = true, 2.3, "four" // 类型分别为 bool、float64、string,由初始化表达式推导
-
多变量声明的灵活性:可以在一条声明语句中声明多个变量,即使它们类型不相同(前提是给出初始化表达式)。
govar f, err = os.Open(name) // os.Open 返回一个文件和一个错误信息
利用函数返回值声明变量:这种方式非常适用于需要同时获取多个返回值的情况。
简短变量声明(:=)
-
在函数内部,Go 提供了更简洁的变量声明方式:
goi := 100 anim := gif.GIF{LoopCount: nframes}
- 自动推导变量类型,不需要显式指定;
- 只能用于局部变量声明,不能在包级使用;
- 注意":="不仅声明变量,还会同时进行初始化。
goi, j := 0, 1
-
与赋值操作的区别:
- ":="用于声明并初始化,至少有一个变量是新声明的;
- "=" 则纯粹是赋值,不引入新的变量;
例如,下面的代码会编译错误,因为没有新的变量被声明
gof, err := os.Open(infile) // ... f, err := os.Create(outfile) // 错误:没有新变量
-
变量混合声明:
如果同一词法域中已有部分变量存在,那么":="会对已有变量进行赋值,同时声明新的变量
goin, err := os.Open(infile) // 声明 in 和 err // ... out, err := os.Create(outfile) // 如果 err 已经存在,则只新声明 out,且对 err 进行赋值
指针
指针是变量存储地址的引用,允许直接操作内存中的数据。
-
使用
&
运算符获取变量的地址gox := 1 p := &x // p 的类型为 *int,存储 x 的内存地址
-
访问指针指向的值 :
使用
*
运算符来读取或修改指针指向的值gofmt.Println(*p) // 读取 p 指向的 x 的值,即 1 *p = 2 // 修改 p 指向的 x 的值为 2
-
指针零值 :所有类型的指针零值为
nil
,这表示指针没有指向任何有效的内存地址。 -
指针比较:可以比较指针是否相等,只有当它们指向同一个变量或都为 nil 时,结果为 true。
govar x, y int fmt.Println(&x == &x, &x == &y, &x == nil) // 输出: true false false
-
指针创建别名
对同一变量取地址或复制指针,都为该变量创建了新的别名,通过 *p 访问到的就是同一个变量的值
-
函数中返回局部变量地址:在 Go 中,返回函数中局部变量的地址是安全的,因为编译器会根据逃逸分析决定将该变量分配在堆上。例如:
gofunc f() *int { v := 1 return &v } fmt.Println(f() == f()) // 输出 false,每次调用返回的地址不同
-
通过传递指针作为参数,可以在函数内部直接更新变量的值
gofunc incr(p *int) int { *p++ // 增加 p 指向的变量的值,但不改变 p 本身 return *p } v := 1 incr(&v) // v 变为 2 fmt.Println(incr(&v)) // 输出 3,同时 v 为 3
new 函数
-
除了使用 var 声明变量外,Go 还提供了内建函数
new
来创建变量。gop := new(int) fmt.Println(*p) // 输出 0(int 的零值) *p = 2 fmt.Println(*p) // 输出 2
-
new(T)
会创建一个 T 类型的匿名变量,将其初始化为 T 的零值,并返回其地址(类型为 *T)。- 它相当于一种语法糖,可以在表达式中使用,而不需要显式声明变量名。
-
每次调用
new
都会返回一个新的变量地址,因此两个 new 的结果通常不同gop := new(int) q := new(int) fmt.Println(p == q) // 输出 false
-
-
注意事项
- new 不是关键字,可以被重新定义,这时在当前作用域内将无法使用内置 new 函数;
- 对于结构体和其他复杂类型,通常推荐使用字面量语法(例如:
&T{...}
)来创建变量,因为更灵活。
逃逸
go
var global *int
func f() {
var x int
x = 1
global = &x // x 的地址赋给全局变量,因此 x 必须在堆上分配
}
func g() {
y := new(int)
*y = 1 // y 是局部变量,但如果不逃逸,编译器可能将其分配在栈上
}
语法特性
- 多重赋值:Go 允许一次对多个变量赋值,常用于交换变量值或同时处理多个返回值。这是 Go 语言简洁语法的重要组成部分。