【十一】Golang 指针
💢欢迎来到张胤尘的开源技术站
💥开源如江河,汇聚众志成。代码似星辰,照亮行征程。开源精神长,传承永不忘。携手共前行,未来更辉煌💥
文章目录
- 指针
- 指针定义
- 指针初始化
- `&` 操作符
- `new` 函数
- 初始化基本类型
- 初始化复合类型
- 指针操作符
- 取地址操作符
- 解引用操作符
- 常见指针
- 空指针
- 函数指针
- 指针函数
- 指针数组
- 数组指针
- 多级指针
- 注意事项
- 避免空指针解引用
- 指针的生命周期
- 指针与并发
- 指针与切片、映射
- 指针与接口
- 可读性与可维护性
指针
在 golang
中,指针是一种特殊的变量,它存储的是另一个变量的内存地址,而不是变量的值本身。通过指针可以直接操作内存地址,从而实现高效的数据访问和修改。
指针定义
指针的类型由 *
和它所指向的变量类型组成。
例如,如果有一个变量 a
是 int
类型,那么指向它的指针类型就是 *int
。语法如下所示:
var p *T
p
是指针变量的名称。*T
表示指针指向的类型是T
。
var a int
var p *int // 定义一个指向 int 类型的指针
指针初始化
在 golang
中,初始化指针变量有两种方式:使用 &
操作符、new
函数,具体使用哪种方式需要根据环境上下文进行确定。
&
操作符
指针可以通过取地址操作符 &
获取变量的内存地址,并将其赋值给指针变量。例如:
package main
import "fmt"
func main() {
var a int = 10
p := &a // 使用 & 获取变量 a 的地址,p 是一个指向 int 的指针
fmt.Printf("a 的值: %d\n", a) // a 的值: 10
fmt.Printf("a 的地址: %p\n", &a) // a 的地址: 0xc0000120d0
fmt.Printf("p 的值: %p\n", p) // p 的值: 0xc0000120d0
fmt.Printf("p 指向的值: %d\n", *p) // p 指向的值: 10
}
a
是一个int
类型的变量,存储值10
。p := &a
表示p
是一个指向a
的指针,&a
获取了变量a
的地址。*p
表示通过指针p
解引用,访问它指向的值。
new
函数
new
函数是一个内置函数,用于分配内存并初始化变量。主要作用是为指定的类型分配零值,并返回指向该值的指针。new
函数是 golang
中用于动态内存分配的常用工具之一,尤其适合在需要初始化指针时使用。
new(T)
T
是需要分配内存的类型。new(T)
返回一个指向类型T
的零值的指针,即返回类型为*T
。
初始化基本类型
new
可以为所有基本数据类型分配内存,代码示例如下所示:
package main
import "fmt"
func main() {
p := new(int)
// p 的类型: *int, p 的值: 0xc000104040, p 指向的值: 0
fmt.Printf("p 的类型: %T, p 的值: %v, p 指向的值: %v\n", p, p, *p)
}
初始化复合类型
以复合数据类型中的结构体为例,代码如下所示:
package main
import "fmt"
type Person struct {
Name string
Age int
}
func main() {
pPerson := new(Person)
pPerson.Name = "Bob"
pPerson.Age = 18
fmt.Printf("%p\n", pPerson) // 0xc0000b2000
fmt.Println(*pPerson) // {Bob 18}
}
指针操作符
在 golang
中,指针操作主要涉及两个操作符:&
和 *
。这两个操作符分别用于获取变量的地址和解引用指针。
取地址操作符
&
操作符用于获取变量的内存地址,并将该地址赋值给一个指针变量。
var p *T = &x
x
是一个变量。T
是变量x
的类型。p
是一个指向类型T
的指针变量,存储了变量x
的地址。
指针初始化小结已经对取地址操作符进行了说明,这里不再赘述。
解引用操作符
*
操作符用于解引用指针,即通过指针访问其指向的变量的值。
var x T = *p
p
是一个指向类型T
(T
表示任意类型)的指针。*p
表示解引用指针p
,获取其指向的变量的值。
例如:
package main
import "fmt"
func main() {
var a int = 10
var p *int = &a // p 是 a 的地址
fmt.Println(*p) // 10
}
常见指针
空指针
空指针是指没有指向任何有效内存地址的指针。在 golang
中,空指针的值是 nil
。
package main
import "fmt"
func main() {
var a *int // a 是一个空指针
if a == nil {
fmt.Println("a is a nil pointer") // a is a nil pointer
}
}
在使用指针过程中需要注意,如果一个指针变量是空指针,那么一定不能对空指针进行解引用操作,否则会导致运行时错误:
package main
import "fmt"
func main() {
var a *int // 未对 a 进行显示初始化,则 a 的初始值为 nil
fmt.Println(*a) // panic: runtime error: invalid memory address or nil pointer dereference
}
在使用之前,应始终检查是否为 nil
:
package main
import "fmt"
func main() {
var a *int // 未对 a 进行显示初始化,则 a 的初始值为 nil
if a == nil {
fmt.Println("a is a nil pointer") // a is a nil pointer
} else {
fmt.Println(*a)
}
}
函数指针
函数指针是指向函数的指针。在 golang
中,函数本身也可以被视为一种类型,可以通过指针调用。
package main
import "fmt"
func add(a, b int) int {
return a + b
}
func main() {
var f func(int, int) int // 定义一个函数指针
f = add // 将函数 add 赋值给 f
fmt.Println(f(3, 4)) // 调用函数指针,输出:7
}
指针函数
指针函数是指返回值为指针的函数。它与函数指针不同,函数指针是指向函数的指针,而指针函数是返回指针的函数。
package main
import "fmt"
func getPointer(a int) *int {
return &a
}
func main() {
p := getPointer(10) // getPointer 是一个指针函数
fmt.Println(*p) // 10
}
指针数组
指针数组是一个数组,其中的每个元素都是指针。
package main
import "fmt"
func main() {
var a int = 10
var b int = 20
var c int = 30
// 定义一个指针数组
var pArray [3]*int
pArray[0] = &a
pArray[1] = &b
pArray[2] = &c
// 10
// 20
// 30
for i := 0; i < 3; i++ {
fmt.Println(*pArray[i])
}
}
数组指针
数组指针是指向整个数组的指针。它与指针数组不同,指针数组是一个数组,其中每个元素是指针;而数组指针是指向一个数组的指针。
package main
import "fmt"
func main() {
var arr [3]int = [3]int{10, 20, 30}
var p *[3]int = &arr // 定义一个指向数组的指针
// 通过数组指针访问数组元素
fmt.Println((*p)[0]) // 输出:10
fmt.Println((*p)[1]) // 输出:20
fmt.Println((*p)[2]) // 输出:30
}
多级指针
多级指针是指,指向指针的指针,例如 **int
或 ***int
。通过多级指针,可以逐层解引用访问最终的值。
package main
import "fmt"
func main() {
var a int = 10
var p *int = &a
var pp **int = &p
var ppp ***int = &pp
fmt.Println(**pp) // 输出:10
fmt.Println(***ppp) // 输出:10
}
注意事项
避免空指针解引用
空指针解引用是 golang
程序中最常见的运行时错误之一。如果尝试解引用一个 nil
指针,程序会崩溃并抛出 panic: runtime error: invalid memory address or nil pointer dereference
。
空指针小结已经对空指针解引用进行了说明,这里不再赘述。
指针的生命周期
指针的生命周期与其指向的变量的生命周期密切相关。如果指向的变量被销毁或超出作用域,指针将变得无效。
需要注意的是,以下代码如果不注意具体使用细节,是有可能会存在严重问题。
package main
import "fmt"
func main() {
p := func() *int {
a := 10
return &a
}()
fmt.Println(*p) // 10
}
在 golang
中,局部变量地址分配时在栈中,当函数执行完毕后,函数栈帧会释放(实际是栈顶指针的移动,并不是真正意义上的内存释放),这意味着它所占用的栈内存可能被其他变量覆盖或重新使用。因此,p
持有的指针可能指向无效的内存区域,解引用 p
(即 *p
)可能导致未定义行为,甚至可能引发运行时错误。
但是真正有意思的是:以上代码如果在编辑器中运行,打印的结果是没有任何的问题!!!
仅仅通过打印观察没有问题,却并不代表这段代码是安全的。其实golang
编译器在某些情况下会对局部变量进行逃逸分析来确定变量是分配在栈上还是堆上。 例如,当编译器检测到局部变量的地址被返回时,它可能会将该变量分配到堆空间,而不是栈空间。这种优化行为使得代码在某些情况下看起来“正常工作”,但这并不是标准行为,而是编译器的特定实现。
将以上代码中的匿名函数编译成汇编代码如下所示:
# 栈检查和栈帧设置
0x0000 00000 TEXT main.main.func1(SB), ABIInternal, $40-0
0x0000 00000 CMPQ SP, 16(R14)
0x0004 00004 PCDATA $0, $-2
0x0004 00004 JLS 65 # 如果栈空间内存不足,则跳转到地址 65 调用 runtime.morestack_noctxt(SB)
0x0006 00006 PCDATA $0, $-1
0x0006 00006 PUSHQ BP
0x0007 00007 MOVQ SP, BP
0x000a 00010 SUBQ $32, SP # 为当前匿名函数分配32字节栈空间
# 函数数据和局部变量初始化
0x000e 00014 FUNCDATA $0, gclocals·g2BeySu+wFnoycgXfElmcg==(SB)
0x000e 00014 FUNCDATA $1, gclocals·EaPwxsZ75yY1hHMVZLmk6g==(SB)
0x000e 00014 MOVQ $0, main.~r0+16(SP) # 返回值寄存器 ~r0 初始化为 0
# 对象创建和初始化
0x0017 00023 LEAQ type:int(SB), AX # 将类型描述符 type:int 的地址加载到寄存器 AX
0x001e 00030 PCDATA $1, $0
0x001e 00030 NOP
0x0020 00032 CALL runtime.newobject(SB) # 调用 runtime.newobject 函数,分配一个新的 int 对象
0x0025 00037 MOVQ AX, main.&a+24(SP) # 将分配的对象地址保存到栈上的 main.&a 变量
0x002a 00042 MOVQ $10, (AX) # 将值 10 存储到新分配的 int 对象中
# 返回值设置和函数结束
0x0031 00049 MOVQ main.&a+24(SP), AX # 将局部变量 a 的地址从栈中加载到寄存器 AX 中
0x0036 00054 MOVQ AX, main.~r0+16(SP) # 将 AX 寄存器中的地址存储到栈中,作为函数的返回值
0x003b 00059 ADDQ $32, SP # 释放当前函数的栈帧,调整栈指针SP
0x003f 00063 POPQ BP # 恢复基指针BP的值
0x0040 00064 RET # 返回调用者
# 分配函数栈帧空间
0x0041 00065 NOP
0x0041 00065 PCDATA $1, $-1
0x0041 00065 PCDATA $0, $-2
0x0041 00065 CALL runtime.morestack_noctxt(SB) # 栈空间是否足够,如果不足,则进行扩展
0x0046 00070 PCDATA $0, $-1
0x0046 00070 JMP 0 # 跳转到地址0,从新开始调用
如上汇编代码所知,分配对象的内存时使用了 golang
运行时的函数 runtime.newobject
。源码如下所示:
源码位置:src/runtime/malloc.go
func newobject(typ *_type) unsafe.Pointer {
return mallocgc(typ.Size_, typ, true)
}
该函数内部又调用了 mallocgc
来进行内存分配。mallocgc
是 golang
运行时的核心内存分配函数,根据对象的大小和特性,决定将对象分配的具体位置:
- 小对象(小于等于 32 KB):通常从每个 P(处理器)的本地缓存(
mcache
)中分配,以提高分配效率。 - 大对象(大于 32 KB):直接从堆(
heap
)中分配。
// Allocate an object of size bytes.
// Small objects are allocated from the per-P cache's free lists.
// Large objects (> 32 kB) are allocated straight from the heap.
//
// ...
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
// ...
}
虽然使用编译器优化的方式经过逃逸分析后,并不会出现未定义的问题,但是这种方式毕竟不是标准的行为,当然如果希望返回一个函数内部的变量的地址,可以使用 new
函数在堆空间中分配内存,这样返回的指针是明确安全的。代码如下所示:
package main
import "fmt"
func main() {
p := func() *int {
a := new(int) // 显式的在堆空间中分配内存
*a = 10
return a
}()
fmt.Println(*p) // 10,这是安全的
}
将以上代码再次编译成汇编代码,如下所示:
0x0000 00000 TEXT main.main.func1(SB), ABIInternal, $40-0
0x0000 00000 CMPQ SP, 16(R14)
0x0004 00004 PCDATA $0, $-2
0x0004 00004 JLS 65
0x0006 00006 PCDATA $0, $-1
0x0006 00006 PUSHQ BP
0x0007 00007 MOVQ SP, BP
0x000a 00010 SUBQ $32, SP
0x000e 00014 FUNCDATA $0, gclocals·g2BeySu+wFnoycgXfElmcg==(SB)
0x000e 00014 FUNCDATA $1, gclocals·EaPwxsZ75yY1hHMVZLmk6g==(SB)
0x000e 00014 MOVQ $0, main.~r0+16(SP)
0x0017 00023 LEAQ type:int(SB), AX
0x001e 00030 PCDATA $1, $0
0x001e 00030 NOP
0x0020 00032 CALL runtime.newobject(SB)
0x0025 00037 MOVQ AX, main.a+24(SP)
0x002a 00042 MOVQ $10, (AX)
0x0031 00049 MOVQ main.a+24(SP), AX
0x0036 00054 MOVQ AX, main.~r0+16(SP)
0x003b 00059 ADDQ $32, SP
0x003f 00063 POPQ BP
0x0040 00064 RET
0x0041 00065 NOP
0x0041 00065 PCDATA $1, $-1
0x0041 00065 PCDATA $0, $-2
0x0041 00065 CALL runtime.morestack_noctxt(SB)
0x0046 00070 PCDATA $0, $-1
0x0046 00070 JMP 0
确实效果是一样的,所以在实际使用中尽可能的采用更标准的写法,避免出现一些不必要或者不可预测的问题出现。
指针与并发
在并发编程中,多个 go routine
同时访问和修改同一个指针指向的值可能导致数据竞争和不可预测的行为。
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
var p *int = new(int) // 分配一个整数的内存,并返回其指针
wg.Add(2)
go func(wg *sync.WaitGroup) {
defer wg.Done()
*p = 10 // 第一个 Goroutine 修改 *p 的值为 10
}(&wg)
go func(wg *sync.WaitGroup) {
defer wg.Done()
*p = 20 // 第二个 Goroutine 修改 *p 的值为 20
}(&wg)
wg.Wait() // 等待两个 Goroutine 完成
fmt.Println(*p)
}
有两种方式可以保护共享数据:使用互斥锁、使用原子操作。
- 互斥锁(
sync.Mutex
)
var mu sync.Mutex
mu.Lock()
*p = 10
mu.Unlock()
- 原子操作
atomic.StoreInt32(p, 10)
更多关于并发相关的知识点请关注文章 《
Golang
并发编程》。
指针与切片、映射
切片和映射的底层实现使用了指针,因此在操作时需要注意底层数组的重新分配问题。
以切片为例,代码如下所示:
package main
import "fmt"
func main() {
slice := []int{1, 2, 3}
fmt.Printf("slice[0] array: %p\n", &slice[0]) // slice[0] array: 0xc0000b2000
fmt.Printf("slice[1] array: %p\n", &slice[1]) // slice[1] array: 0xc0000b2008
fmt.Printf("slice[2] array: %p\n", &slice[2]) // slice[2] array: 0xc0000b2010
p := &slice[0]
slice = append(slice, 4, 5) // 扩容机制导致底层数组重新分配
fmt.Printf("slice[0] array: %p\n", &slice[0]) // slice[0] array: 0xc0000b8000
fmt.Printf("slice[1] array: %p\n", &slice[1]) // slice[1] array: 0xc0000b8008
fmt.Printf("slice[2] array: %p\n", &slice[2]) // slice[2] array: 0xc0000b8010
fmt.Printf("slice[3] array: %p\n", &slice[3]) // slice[3] array: 0xc0000b8018
fmt.Printf("slice[4] array: %p\n", &slice[4]) // slice[4] array: 0xc0000b8020
fmt.Println(*p) // 可能会导致未定义行为
}
如果将上面的代码运行,经过多次的输出结果可能会是正确的,但是需要注意的却恰恰是这一点:因为切片、映射的底层数组扩容后,数组的地址发生了变化,但是原本的地址上的数据并没有清理,如果此时仍然使用旧的地址访问数据,可能会获取到错误的结果或者未定义。
如果需要在切片或映射上进行复杂操作,建议在操作完成后重新获取指针。另外尽量避免直接操作切片或者映射的底层数组。
package main
import "fmt"
func main() {
slice := []int{1, 2, 3}
p := &slice[0]
slice = append(slice, 4, 5) // 扩容机制导致底层数组重新分配
p = &slice[0] // 重新为指针 p 赋值
fmt.Println(*p) // 1
}
关于切片、映射相关的更多知识点请关注文章 《
Golang
切片》、《Golang
映射》。
指针与接口
在 golang
中接口可以存储任何类型的值,包括指针。如果接口存储的是指针,解引用时需要特别小心。
例如:
package main
import "fmt"
func main() {
var i interface{} = nil
*i.(*int) = 10 // panic: interface conversion: interface {} is nil, not *int
fmt.Println(*i.(*int))
}
首先在操作接口中的指针时,需要先进行类型断言,确保接口值是正确的指针类型,如果接口值为 nil
,类型断言会失败,因此需要先检查接口是否为 nil
。
package main
import "fmt"
func main() {
var i interface{} = new(int)
if i == nil {
fmt.Println("i is nil")
} else {
*i.(*int) = 10 // 显式类型断言
fmt.Println(*i.(*int)) // 10
}
}
关于接口相关的更多知识点请关注文章 《
Golang
接口》。
可读性与可维护性
虽然指针提供了强大的功能,但过度使用指针会使代码难以理解和维护。在某些情况下,可以使用其他方式(如值类型、通道等)来实现相同的功能。另外也同样避免使用多级指针,除非确实需要。
🌺🌺🌺撒花!
如果本文对你有帮助,就点关注或者留个👍
如果您有任何技术问题或者需要更多其他的内容,请随时向我提问。