目 录CONTENT

文章目录
go

【Go】指针

hxuanyu
2025-12-29 / 0 评论 / 0 点赞 / 42 阅读 / 0 字

指针类型定义

指针类型依赖于某个特定的类型而存在。例如,如果有一个整型 int​,那么它对应的指针类型就是 *int​;没有 int​ 类型,就不会有 *int​ 类型。这里的 int​ 被称为 *int 指针类型的基类型(base type)。

可以使用以下方式声明指针类型变量:

var p *T

Go 中有一种特殊的指针类型不需要基类型,即 unsafe.Pointer​,类似于 C 语言中的 void*​,用于表示通用指针类型。任意指针类型都可以显式转换为 unsafe.Pointer​,也可以从 unsafe.Pointer 显式转换回任意指针类型(前提是转换结果在语义上是安全且有效的)。

例如:

var p *T
var p1 = unsafe.Pointer(p) // 将任意指针类型显式转换为 unsafe.Pointer
p = (*T)(p1)               // 将 unsafe.Pointer 显式转换为任意指针类型

unsafe.Pointer 是 Go 的高级特性,在 Go 运行时与标准库中均有使用,但它会绕过一部分类型与内存安全检查,应谨慎使用。

如果一个指针类型变量未被赋予初始值,那么它的值为 nil

var p *T
fmt.Println(p == nil) // true

如果要给指针类型变量赋值,可以使用取地址操作符 &

var a int = 13
var p *int = &a // 给整型指针变量 p 赋初始值

这一行代码将变量 a​ 的地址赋值给指针变量 p。只能使用与基类型匹配的地址为对应的指针类型变量赋值,否则 Go 编译器会报错:

var b byte = 10
var p *int = &b // 编译器错误:cannot use &b (value of type *byte) as type *int in variable declaration

对于非指针类型变量,Go 在其对应的内存单元中直接存储该变量的值;但对于指针类型变量,指针变量的内存单元中存储的是其基类型变量所在内存单元的地址:

image

由于指针类型变量存储的是地址,因此其大小与基类型大小无关,而取决于平台的指针宽度:

// ptrsize.go
package main

import "unsafe"

type foo struct {
	id   string
	age  int8
	addr string
}

func main() {
	var p1 *int
	var p2 *bool
	var p3 *byte
	var p4 *[20]int
	var p5 *foo
	var p6 unsafe.Pointer
	println(unsafe.Sizeof(p1)) // 在 64 位平台通常输出:8
	println(unsafe.Sizeof(p2)) // 在 64 位平台通常输出:8
	println(unsafe.Sizeof(p3)) // 在 64 位平台通常输出:8
	println(unsafe.Sizeof(p4)) // 在 64 位平台通常输出:8
	println(unsafe.Sizeof(p5)) // 在 64 位平台通常输出:8
	println(unsafe.Sizeof(p6)) // 在 64 位平台通常输出:8
}

无论指针的基类型是什么,在同一平台上不同类型的指针大小一致;例如在 x86-64 平台上,地址长度通常为 8 字节。

unsafe​ 包中 Sizeof​ 函数的原型为:func Sizeof(x ArbitraryType) uintptr​。其返回值类型是 uintptr​,这是 Go 预定义的整数类型。官方文档对 uintptr​ 的描述是:一个足够大的无符号整型,可以存储任何指针的位模式。 因此,在某个平台上 uintptr 的大小通常就等同于该平台的指针大小。

一旦指针变量被正确赋值(指向某一合法类型的变量),就可以通过该指针读取或修改其指向的变量值:

var a int = 17
var p *int = &a
fmt.Println(*p) // 输出:17
(*p) += 3
fmt.Println(a) // 输出:20
image

通过指针变量读取或修改其指向的内存地址上的变量值的操作称为指针的​解引用​(dereference)。解引用的形式是在指针变量前加一个 *​。解引用操作访问的是指针所指向的值,而不是指针变量自身的值。若要输出指针变量自身保存的地址,应使用 Printf​ 配合 %p

fmt.Printf("%p\n", p)

指针变量可以改变其指向的内存单元,语法上表现为重新给指针变量赋值:

var a int = 5
var b int = 6
var p *int = &a  // 指向变量 a
println(*p)      // 输出 5
p = &b           // 改为指向变量 b
println(*p)      // 输出 6

多个指针变量可以同时指向同一个变量,这意味着通过其中一个指针修改变量后,另一个指针解引用也会反映出修改结果:

var a int = 5
var p1 *int = &a
var p2 *int = &a
(*p1) += 5
println(*p2) // 输出 10

指向指针的指针

参考以下例子:

package main

func main() {
	var a int = 5
	var p1 *int = &a
	println(*p1) // 输出:5

	var b int = 55
	var p2 *int = &b
	println(*p2) // 输出:55

	var pp **int = &p1
	println(**pp) // 输出:5

	pp = &p2
	println(**pp) // 输出:55
}

以上例子中,p1​ 和 p2​ 是 *int​ 类型,分别指向变量 a​ 和 b​。pp​ 是 **int​ 类型,存储的是某个 *int​ 指针变量本身的地址(例如 &p1​ 或 &p2)。其关系如下图所示:

image

**int​ 被称为二级指针(指向指针的指针),而 *int 是一级指针。

对一级指针解引用一次,得到它指向的变量值;对二级指针解引用一次,得到它指向的一级指针变量(即某个 *int 值):

println((*pp) == p1) // 当 pp == &p1 时输出:true

对二级指针解引用两次,等效于对其所指向的一级指针解引用一次:

println((**pp) == (*p1)) // 当 pp == &p1 时输出:true
println((**pp) == a)     // 当 pp == &p1 时输出:true

二级指针常用于“改变某个指针变量本身的值”(也就是改变其指向)。在同一个函数内改变指针的指向很容易,直接重新赋值即可;但如果需要在函数调用中改变调用方的指针变量,就需要把“指针变量的地址”(即二级指针)传入函数:

// ch10/passptr2ptr.go
package main

func foo(pp **int) {
	var b int = 55
	var p1 *int = &b
	*pp = p1
}

func main() {
	var a int = 5
	var p *int = &a
	println(*p) // 输出:5
	foo(&p)
	println(*p) // 输出:55
}

二级指针在 Go 中并不常见。很多情况下可以用返回值、闭包或更高层的抽象(如切片、map、接口等)更清晰地表达需求。三级及以上指针会显著降低可读性,实际开发中应谨慎使用。

指针的用途和使用限制

指针类型的用途

尽管 Go 带有垃圾回收机制,但指针在 Go 中仍然很重要:内存可达性分析、逃逸分析与垃圾回收等机制的核心就是追踪指针关系。

在语法层面,与 C 语言相比,Go 对显式指针的依赖更少,主要因为 Go 提供了更高级的复合类型(如切片、map、chan 等),并在实现层面隐藏了大量指针操作细节,因此开发者通常不需要像 C 那样频繁手写指针逻辑。

指针在 Go 中最直接的意义是“共享与可变性”:使用 *T 作为方法接收者、函数参数或返回值,通常是为了在多个作用域之间共享同一份数据,并允许原地修改。

此外,传递指针的拷贝成本通常为常数级(一个机器字大小),在传递大型结构体时可能比按值传递更可控。但也需要结合逃逸分析、对象生命周期与并发安全综合考量。

指针类型的限制

限制显式指针类型转换

在 C 语言中,可以进行如下显式指针类型转换:

#include <stdio.h>

int main() {
    int a = 0x12345678;
    int *p = &a;
    char *p1 = (char*)p;
    printf("%x\n", *p1);
}

但在 Go 中,直接把 *int​ 转成 *byte 会导致编译错误:

package main

import (
	"fmt"
)

func main() {
	var a int = 0x12345678
	var pa *int = &a
	var pb *byte = (*byte)(pa) // 编译器错误:cannot convert pa (variable of type *int) to type *byte
	fmt.Printf("%x\n", *pb)
}

如果确实需要进行这种转换,可以借助 unsafe.Pointer,但代码的正确性与安全性需要由开发者自行保证:

// ch10/unsafeconvert.go
package main

import (
	"fmt"
	"unsafe"
)

func main() {
	var a int = 0x12345678
	var pa *int = &a
	var pb *byte = (*byte)(unsafe.Pointer(pa)) // 正确(但不安全)
	fmt.Printf("%x\n", *pb)                    // 在小端机器上通常输出:78
}

不支持指针运算

在 C 语言中,指针运算允许开发者遍历数组等:

#include <stdio.h>

int main() {
    int a[] = {1, 2, 3, 4, 5};
    int *p = &a[0];
    for (int i = 0; i < sizeof(a)/sizeof(a[0]); i++) {
        printf("%d\n", *p);
        p = p + 1;
    }
}

然而指针运算也是许多内存安全问题的来源。为提高安全性,Go 在语法层面不支持指针运算:

package main

func main() {
	var arr = [5]int{1, 2, 3, 4, 5}
	var p *int = &arr[0]
	println(*p)
	p = p + 1 // 编译器错误:invalid operation: p + 1 (mismatched types *int and untyped int)
	println(*p)
}

如确有需要,可以通过 unsafe​ 配合 uintptr​ 做地址计算(应确保对象仍然存活且遵循 unsafe 的使用约束):

// ch10/unsafeiterate.go
package main

import "unsafe"

func main() {
	var arr = [5]int{11, 12, 13, 14, 15}
	p := &arr[0]
	for i := 0; i < len(arr); i++ {
		p1 := (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(p)) + uintptr(i)*unsafe.Sizeof(*p)))
		println(*p1)
	}
}

0
博主关闭了所有页面的评论