指针类型定义
指针类型依赖于某个特定的类型而存在。例如,如果有一个整型
int,那么它对应的整型只怎是*int,没有int类型,就不会有*int类型。这里的int被称为*int指针类型的基类型。
可以使用以下方式声明指针类型变量:
var p *T
Go中有一种特殊的指针类型不需要基类型,即unsafe.Pointer,类似于C语言中的void*,用于表示通用指针类型,意味着任何指针类型都可以显式转换为unsafe.Pointer,反之亦然。
例如:
var p *T
var p1 = unsafe.Pointer(p)// 将任意指针类型显式转换为`unsafe.Pointer`
p - (*T)(p1) // 将unsafe.Pointer显式转换为任意指针类型
unsafe.Pointer是Go的一个高级特性,在Go运行时与标准库中都有广泛应用。
如果一个指针类型变量未被赋予初始值,那么它的值为nil。
vat p *T
fmt.Println(p == nil) // true
如果要给指针类型变量赋值,可以使用以下方式:
var a int = 13
var p *int = &a // 给整型指针变量p赋初始值
在这个示例中,使用&a作为*int指针类型变量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在其对应的内存单元中直接存储该变量的值,但对于指针类型,Go为指针变量分配的内存单元中存储的是指针的基类型变量对应的内存单元的地址:

由于指针类型变量存储的是内存单元的地址,因此其大小与基类型大小无关,而是取决于系统地址表示的长度:
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)) // 输出:8
println(unsafe.Sizeof(p2)) // 输出:8
println(unsafe.Sizeof(p3)) // 输出:8
println(unsafe.Sizeof(p4)) // 输出:8
println(unsafe.Sizeof(p5)) // 输出:8
println(unsafe.Sizeof(p6)) // 输出:8
}
无论指针的基类型是什么,在同一个平台上不同类型的指针大小是一致的,例如在x86-64平台上,地址长度都是8字节。
unsafe包中Sizeof函数的原型为:func Sizeof(x ArbitraryType) unitptr,这个函数返回值的类型是unitptr,这是Go预定义的一个标识符,官方文档描述unitptr为:一个足够大的整型,可以容纳任何指针的比特模式。 这句话可以理解为:在Go中,uniptr类型的大小就代表指针类型的大小。
一旦指针变量被正确赋值,即指向某一合法类型的变量,就可以通过该指针读取或修改其指向的内存单元所代表的基类型变量:
var a int = 17
vat p*int = &a
fmt.Println(*p) // 输出:17
(*p) += 3
fmt.Println(a) // 输出:20

通过指针变量读取或修改指向的内存地址上的变量值的操作被称为指针的解引用。 这种操作的形式是在指针类型变量前加一个星号,通过解引用操作输出或修改的并不是指针变量本身的值,而是它指向的内存单元中的值。如果要输出指针变量自身的值,即其所指向的内存单元地址,可以使用Printf函数结合%p格式化标识符来实现:
fmt.Println("%p\n", p)
指针变量可以改变其指向的内存单元,在语法上表现为重新为指针变量赋值:
var a int = 5
var b int = 6
var p *int = &a // 指向变量a所在的内存单元
println(*p) // 输出变量a的值,即与
p = &b // 指向变量b所在的内存单元
println(*p) // 输出变量b的值,即6
多个指针变量可以同时指向一个变量的内存单元,这意味着通过其中一个指针变量对内存单元的修改都可以通过另一个指针变量的解引用反映出来:
var a int = 5
var p1 *int = &a // p1指向变量a所在的内存单元
var p2 *int = &a // p2指向变量a所在的内存单元
(*p1) += 5 // 通过p1将变量a的值增加5
println(*p2) // 输出10 ,表明对变量a的修改可以通过p2解引用反映出来
指向指针的指针
参考以下例子:
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
}
在以上例子中,声明了两个*int类型的指针p1和p2分别指向整型变量a和b。同时,还声明了一个**int类型的指针变量pp,它的初始值为指针变量p1的地址,之后,将p2的地址赋值给pp。可以通过下图来反应示例中的操作:

可以看到,**int类型的变量pp中存储的是*int类型变量的地址。这与之前提到的*int类型变量存储的是int类型变量的地址原理相同。**int被称为二级指针,也就是指向指针的指针,而*int则是一级指针。
对以及指针解引用一次,得到的是它指向的变量,对二级指针解引用一次,得到的是指向的指针变量:
println((*pp) == p1) // 输出:true
对二级指针解引用两次,等效于对其指向的一级指针解引用一次:
println((**pp) == (*p1)) // 输出:true
println((**pp) == a) // 输出: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中很少使用,至于三级或更多级指针,在实际开发中更要慎用,会大幅降低Go代码的可读性。
指针的用途和使用限制
指针类型的用途
尽管Go带有垃圾回收机制,但指针在Go中仍然很重要,尤其是内存管理与垃圾回收两个关键机制,它们只关心指针。
在语法层面,和C语言相比,指针的使用量要少很多,主要是因为Go提供了更为灵活和高级的复合类型,如切片、map等,并将使用指针的复杂性隐藏在底层实现中,因此,Go开发者不需要在语法层面通过指针来实现高级复合类型的功能。
无论是在Go中,还是在其他支持指针的变成语言中,指针存在的意义是“可变性”。在Go中,使用*T类型的变量调用方法、以*T类型作为函数或方法的形式参数、返回*T类型的返回值等,都是因为指针能够改变其所指向的内存单元的值。
此外,指针的另一个好处是传递开销为常数级,可控且可预测。Go对指针类型也施加了一定的限制,用来保证安全编程。
指针类型的限制
限制显式指针类型转换
在C语言中,可以进行以下显示的指针类型转换:
#include <stdio.h>
int main() {
int a = 0x12345678;
int *p = &a;
char *p1 = (char*)p; // 将一个整型指针显式转换为一个字符类型指针
printf("%x\n", *p1);
}
但在Go中,类似的显式指针转换会导致编译器错误:
package main
import (
"fmt"
"unsafe"
)
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
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 // 编译器错误:cannot convert 1 (untyped int constant) to *int
println(*p)
}
同样Go在unsafe.Pointer中提供了相关解决方式:
// ch10/unsafeiterate.go
package main
import "unsafe"
func main() {
var arr = [5]int{11, 12, 13, 14, 15}
var p *int = &arr[0]
var i uintptr
for i = 0; i < uintptr(len(arr)); i++ {
p1 := (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(p)) + i*unsafe.Sizeof(*p)))
println(*p1)
}
}
评论区