1.Go指针的基础
本文假设读者已经有部分go基础。
1.1什么是go的指针?
Go语言中的指针不能进行偏移和运算,是安全指针。
搞明白Go语言中的指针需要先知道3个概念:指针地址,指针类型和指针取值。
1.1.1 go中的指针
Go语言中的函数传参都是值拷贝,可以通过指向该变量地址的指针变量来修改数据。
Go的指针基础操作只需要:&
取地址和*
根据地址进行取值
1.1.2 指针地址和指针类型与取值
每个变量在运行时会有一个内存地址,根据不同架构指针有32位和64位地址的区别。
Go语言中的值类型int, float, bool, string, array, struct
都有对应的指针类型。
如:*int, *int32, *string
num := 1027
var p = &num
// var p *int= &num
fmt.Printf("%p, %p, %p\n", p, &p, *(&p))
// 0xc00000a0c8, 0xc000072028, 0xc00000a0c8
fmt.Printf("%T, %T, %T\n", p, &p, *(&p))
// *int, **int, *int
**int
,存储int
类型的指针的指针变量。
1.2 Go指针常见细节
1.2.1 普通数据类型的副本机制
func changeNum(num int) {
fmt.Printf("change: %d %p\n", num, &num)// change: 1027 0xc0001920b0
num = 1025
fmt.Printf("change: %d %p\n", num, &num)// change: 1025 0xc0001920b0
}
func main() {
num := 1027
fmt.Printf("main: %d %p\n", num, &num) // main: 1027 0xc000192068
changeNum(num)
}
拷贝新数据,分配新内存。
1.2.2 指针遵循副本机制实现数据修改
func changeNum(p *int) {
fmt.Printf("change: %d %p %p\n", *p, p, &p)
// change: 1027 0xc00000a0c8 0xc000072028
}
&p
可以看出,指针被拷贝了一份赋值给了p
,但是仍然指向同一个地址
1.2.3 空指针及判断
func main() {
var p *int
fmt.Println(p) // <nil>
if p != nil {
fmt.Println("not nil")
} else {
fmt.Println("nil")
}
}
1.2.4 new的用途
用于内存分配,创建对应大小的空间,然后将该空间的地址赋值给p
var p *int = new(int)
p := new(int)
1.2.5 make的用途
make用于内存分配,只用于slice, map, chan
的创建
slice, map, channel
的使用,需要make进行初始化
1.2.6 数组指针
func change(p *[2]int) {
(*p)[0] = 1027
for index, value := range *p {
fmt.Printf("index: %d, value: %d\n", index, value)
}
}
func main() {
arr := [2]int{1125, 1125}
change(&arr)
fmt.Println(arr)
}
数组量过大,给函数传递数组的时候栈帧接受不了如此大的数据量,所以需要数组指针进行传输。
1.2.7 指针数组的作用
func main() {
var rank [5]int = [5]int{30, 50, 32, 12, 78}
var prank [5]*int
for i := 0; i < 5; i++ {
prank[i] = &rank[i]
}
for i := 0; i < len(prank); i++ {
for j := 0; j < len(prank)-i-1; j++ {
if *prank[j] < *prank[j+1] {
prank[j], prank[j+1] = prank[j+1], prank[j]
}
}
}
fmt.Println(rank)
for i := 0; i < len(prank); i++ {
fmt.Println(*prank[i], prank[i])
}
}
排序的元素包含复杂结构或对象,通过指针排序可以避免不必要的内存操作
1.2.8 二级指针修改指针参数
在函数中,如果你想修改传入的指针所指向的值,你需要使用二级指针作为参数。这是因为普通的一级指针只能修改其指向的数据,而不能修改其本身的值
func change(p **int) {
*p = &num2
}
func main() {
p := &num1
change(&p)
}
1.2.9 指向结构体的指针
go结构体是值类型,非引用传递
go语言允许我们可以像访问普通结构体一样访问结构体指针变量,不必使用->
原因是Go为方便访问结构体指针的成员变量,使用语法糖(Syntactic sugar)技术将instance.attribute
形式转换为(*instance).attribute
ins := &T{} <==> new(T)
当然也可以使用函数封装进行初始化。
type CTFer struct {
direction string
rank int
}
func newCTFer(direction string, rank int) *CTFer {
return &CTFer{direction: direction, rank: rank}
}
func main() {
pwn1 := newCTFer("pwn1", 10)
fmt.Println(pwn1)
}
1.2.10 变量逃逸
在Go语言中,“变量逃逸”(Escape Analysis)是指编译器分析变量生命周期的过程,以决定一个局部变量是否需要被分配在堆上,而不是栈上。逃逸分析是Go的编译器优化的一部分,它帮助管理内存分配,提高程序的运行效率。
逃逸分析的基本原则
- 局部变量:如果一个变量只在函数内部使用,并且在函数返回后不再被访问,那么这个变量可以安全地分配在栈上,因为它不会“逃逸”出函数的作用域。
- 函数返回的变量:如果一个局部变量被函数返回,或者通过引用传递给其他函数,那么这个变量可能在函数外部被访问,也就是说它逃逸出了函数的作用域。这种情况下,变量通常会被分配在堆上,以确保其生命周期超出函数调用。
- 全局变量:全局变量总是逃逸的,因为它们在整个程序的生命周期内都是可访问的。
- 循环中分配的变量:在循环中分配的变量,如果它们的生命周期超出了单次循环迭代,通常也会逃逸到堆上。
逃逸分析的重要性
- 减少垃圾回收的压力:通过避免不必要的堆分配,可以减少垃圾回收的频率和时间,从而提高程序的运行效率。
- 提高性能:栈分配通常比堆分配更快,因为栈的内存分配和释放是自动的,不需要复杂的垃圾回收机制。
go run -gcflags "-m -l" .\main.go
func example() *int {
var x int = 10
return &x // x逃逸了,因为它的地址被返回给了函数的调用者
}
func main() {
p := example()
fmt.Println(*p)
}
1.2.11 nil 空值/零值
Go语言中,指针、切片、映射、通道、函数和接口的零值是nil。
Go的nil
与其他语言中的null
由很多不同点
nil的类型为(T)(nil)
nil不可比较
fmt.Println(nil == nil)
//invalid operation: nil == nil (operator == not defined on untyped nil)
无法比较的类型的值是非法的
var s1 []int
var s2 []int
fmt.Println(s1 == s2)
// invalid operation: s1 == s2 (slice can only be compared to nil)
每个nil占用的内存大小不同
以切片nil
值为例子
type slice struct {
array unsafe.Pointer
len int
cap int
}
slice
结构体定义如上,每个结构体字段占8
字节,共占24
字节
1.2.12 new与make的差异
make参考官方注释
// The make built-in function allocates and initializes an object of type
// slice, map, or chan (only). Like new, the first argument is a type, not a
// value. Unlike new, make's return type is the same as the type of its
// argument, not a pointer to it. The specification of the result depends on
// the type:
//
// Slice: The size specifies the length. The capacity of the slice is
// equal to its length. A second integer argument may be provided to
// specify a different capacity; it must be no smaller than the
// length. For example, make([]int, 0, 10) allocates an underlying array
// of size 10 and returns a slice of length 0 and capacity 10 that is
// backed by this underlying array.
// Map: An empty map is allocated with enough space to hold the
// specified number of elements. The size may be omitted, in which case
// a small starting size is allocated.
// Channel: The channel's buffer is initialized with the specified
// buffer capacity. If zero, or the size is omitted, the channel is
// unbuffered.
func make(t Type, size ...IntegerType) Type
相同点是第一个参数都传入的是类型,而不是值。
区别在于make
的返回类型与它的参数类型相同,而不是指向该类型的指针。
make仅限用于slice, map, channel
三个类型
p := new([]int)
v := make([]int, 10, 20)
fmt.Printf("%T %v\n", p, p) // *[]int 0xc000118020
fmt.Printf("%T %v\n", v, p) // []int &[]
new与make总结
new(T)
返回T
的指针*T
并指向T的零值make(T)
返回初始化的T
, 只能用于slice, map, channel
, 要获取一个显示的指针,使用new
进行分配,或者显式地使用一个变量的地址。- new函数分配内存,make函数初始化。
p := new([]int) // &[]
// *p = append(*p, 1) 取消注释此处可成功
(*p)[0] = 19
// index out of range [0] with length 0
1.3 指针的注意事项
指针的初始值\零值\默认值为nil
1.3.1 不能操作没有合法指向的内存
var p *int
*p = 1027
// panic: runtime error: invalid memory address or nil pointer dereference
var p *int
p = new(int)
*p = 3
fmt.Println(*p) // 3
1.3.2 值类型与引用类型
值类型直接存储值,通常分配在栈上
引用类型存储一个地址,地址对应的空间用于存储值,通常分配在堆上。没有任何变量引用时,该地址对应的数据空间就变成了一个垃圾,由GC来回收