Pointer
指针
在 Go 语言中,一切都是按值传递,但为了高效地操作大型数据、修改函数外的变量、避免拷贝浪费,可以使用指针。
指针是一种存放内存空间地址的类型,总会指向一块特定的内存空间。通过指针,可以直接访问其对应的内部的属性,但是指针不能直接参与运算(unsafe.Pointer)。
使用
*T 是指向 T 类型值的指针类型,如:
// 声明了一个 *int 类型指针,初始值为 nil
var p *int;
取地址: 使用 & 运算符获取变量地址,得到一个指针。如:
x := 1
// p 的类型是 *int,值是 x 的内存地址
p := &x
解引用:使用 * 运算符读取或修改指针所指内存地址的值。如:
fmt.Printf(*p) // 1
// 修改值
*p = 2
fmt.Printf(x) // 2
动态分配内存
通过new(type)分配一个内存空间,会得到一个内存空间的指针。
package main
import "fmt"
func main() {
a := new(int)
*a = 1
fmt.Println(*a) // 1
b := 100
a = &b
fmt.Println(*a) // 100
}
如上,指针 a 的内存地址被动态的分配,指针内部的值由 1 变成 100。
修改变量值
Go 函数参数默认按值传递。若要在函数里修改调用者的变量,需要传递指针。
直接传递指针可以避免大结构体或数组的拷贝,提升性能。
package main
import "fmt"
func test(a *int) {
*a = 100
}
func main() {
a := 1
test(&a)
fmt.Println(a) // 100
}
结构体指针
很常见的用法是让函数或方法接收结构体指针,以便在方法中修改其字段,或避免值拷贝。
type Point struct{ X, Y int }
func (p *Point) Move(dx, dy int) {
p.X += dx
p.Y += dy
}
func main() {
pt := Point{1, 2}
pt.Move(10, 20)
fmt.Println(pt) // {11, 12}
}
- 值接收者在方法调用时会拷贝一个副本,操作不会影响原对象。
- 指针接收者则能直接修改原对象,并且对大型结构体更高效。
引用类型
Go 的 slice、map、chan 和 interface 本身内部已经包含指针,多为引用传递。
func modifySlice(s []int) {
s[0] = 100
}
func main() {
sl := []int{1, 2, 3}
modifySlice(sl)
fmt.Println(sl) // [100 2 3]
}
指针运算
Go 不允许在指针上做算术运算。如果要访问数组或切片元素,应使用下标。
arr := [3]int{10, 20, 30}
p := &arr[0]
// var p2 = p + 1 // 编译错误
fmt.Println(arr[1]) // 20
野指针
指针不明确指向任何空间。声明一个指针变量,没有初始化,但未初始化的指针是可以访问的,访问的结果就是野指针。
nil / 0x0就是野指针的值表现。
package main
import "fmt"
func main() {
var a *int
fmt.Println(a) // <nil>
fmt.Printf("%p", a) // 0x0
}
通过new(type)返回的指针不是野指针,它会分配一个真实的空间,并且给出一个默认值,这种方式推荐使用:
package main
import "fmt"
func main() {
a := new(int)
fmt.Println(a)
fmt.Println(*a) // 0
}
最佳实践
- 在解引用之前务必确认指针非
nil,或在设计上保证它已经初始化。 - 若一个类型有任意一个方法使用了指针接收者,建议该类型的所有方法都用指针接收者,以避免值拷贝和接收者一致性问题。
- 对小型或简单的值类型(如
int、小结构体)频繁使用指针可能会增加 GC 压力,按值传递通常更简单、安全,不要过度使用指针。
可寻址
Go 中的指针,不能对常量或字面量取地址,必须是有地址的可寻址变量、数组元素、切片元素、结构体字段等。
一个表达式如果满足以下条件,则被认为是可寻址的,可以对其取地址:
// 变量
var x int
p := &x
// 指针解引用
type T struct {
A int
}
var t *T = &T{ 1 }
p := &t.A // ==> &((*t).A)
// 数组元素
arr := [3]int{10, 20, 30}
p := &arr[1] // arr[1] 可寻址
// 切片元素
sl := []string{"a", "b", "c"}
p := &sl[2] // sl[2] 可寻址
// 结构体字段
type S struct { F float64 }
var s S
p := &s.F // OK
// 组合字面量前加 &
p := &struct{ X int }{X: 42}
有些表达式虽然看起来有值,但是不能取地址,会编译报错:
// 常量或字面量
// invalid operation: cannot take address of 42
p := &42
// invalid operation: cannot take address of "foo"
p := &"foo"
// 函数调用或返回值
func f() int { return 1 }
// invalid operation: cannot take address of f()
p := &f()
// 运算结果
var x, y int
// invalid operation: cannot take address of x + y
p := &(x + y)
// 切片本身或映射索引以外的复合表达式
sl := []int{1,2,3}
// invalid operation: cannot take address of sl[:] (a slice value)
p := &sl[:]
m := map[int]int{1:10}
// invalid operation: cannot take address of m[1]
p := &m[1]
// 类型转换结果
var f float64 = 3.14
// invalid operation: cannot take address of int(f)
p := &int(f)
Go 的设计目标之一就是保证内存安全和简单的逃逸分析。只有确实在某个已命名的位置存储的数据才能被取地址并长期存活。常量、字面量或中间运算结果等都没有固定地址,它们可能只是存在寄存器或栈帧中,因此不能被引用。
常量、字面量并不是在运行时固定存在于某个内存单元里的变量,而是编译期就已经确定的值,编译器会把它们内联到指令或临时存储中,而不会给它们开辟一个可寻址、可修改的内存槽。
基于语言的设计个效率考量:
- 如果每个常量和字面量都要占用一个全局或局部内存位置,那么程序里写十几个相同的字符串字面量就得分配多份或管理一个全局常量池,反而更复杂。Go 选择在需要时复用字面量在只读段的一份拷贝,或者直接内联到指令流中。
- 常量本质上不可修改。即使给它分配了地址,运行时也不应该允许写入。语言设计上把它们当值的概念,而不是状态的概念,去除取地址这一操作可以让开发者不误以为它是可写的。
复合字面量虽然在语义上会生成一个临时对象,但编译器要么把它放到只读数据段(对结构体、数组、字符串字面量),要么在运行时在栈上或作为表达式临时变量分配一个不可寻址的临时值,再在同一个表达式里立刻使用。
这是 Go 语言的特例,允许对复合字面量直接取地址,但这并不意味着该字面量在源代码里就有个固定地址,编译器会为这一次表达式在栈/堆上分配空间,然后把指针赋给变量。
nil
基本认识
nil是 Go 语言中的零值或空值,引用类型的零值,所以也是指针的零值。nil不是一种数据类型,也不是关键字,而是 Go 语言的标识符。
在使用前应先判断是否为 nil,确保已经正确初始化。
var p *int // nil
if p != nil {
fmt.Println(*p)
}
// 或者先分配内存
var q = new(int)
*q = 42
new(T) 会分配一块零值内存,返回 *T,而 make 用于创建 slice、map、chan 等引用类型,返回的是初始化后的本身。
零值
Go语言中各种类型的的零值如下:
| 类型 | 默认值 | 零值 |
|---|---|---|
| bool | false | false |
| number | 0 | 0 |
| string | "" | "" |
| pointer | nil | |
| channel | nil | |
| func | nil | |
| interface | nil | |
| slice | [] | nil |
| map | map[] | nil |
| struct | - |
struct没有零值,因为struct是数据的结构,内部有结构的装载具体类型。- 只有零值为
nil的才能赋值nil。
非类型
考虑如下示例:
package main
import "fmt"
func main() {
var a *int = nil
fmt.Printf("%T", a) // *int
fmt.Printf("%T", nil) // <nil>
}
虽然在获取nil的类型时输出nil,但仍不能证明nil是一种类型。
类型推断
考虑如下示例:
package main
func main() {
// use of untyped nil in assignment
a := nil
}
此种写法是一种需要Go进行类型推断的写法,当赋值nil时,而nil却不是一种数据类型,所有没办法进行准确的类型推断。
nil作为某些类型的零值,且不止一种,该推断成哪种类型的零值也不确定。
赋值条件
赋值零值nil的两个条件:
- 显式地定义变量的类型。
- 类型必须符合
nil为该类型零值的条件。
package main
func main() {
// var a int = nil
var b []int = nil
}
非关键字
nil不是Go语言的关键字,因此可作为变量,但尽量不要使用:
package main
import "fmt"
func main() {
nil := 1
fmt.Println(nil)
}
相等性判断
引用在Go语言中是不能做比较的,因此相等性判断也不能做比较:
package main
import "fmt"
func main() {
var s1 []int = nil
var s2 []int = nil
// invalid operation: s1 == s2
fmt.Println(s1 == s2)
}
nil map
未初始化的map,Go语言底层会分配存储空间,并不是nil,但是不能直接增加属性。
package main
import "fmt"
func main() {
var m map[int]int
// assignment to entry in nil map
m[1] = 1
fmt.Println(m)
fmt.Printf("%p", &m)
}
通过make创建的长度为0的map称为空map,可以直接进行属性操作。
package main
func main() {
var m = make(map[int]int, 0)
m[1] = 1
}