「这是我参与11月更文挑战的第11天,活动详情查看:2021最后一次更文挑战」
如果你有接触过C语言, 相信你会对其指针功能的强大印象深刻, 但大多数情况下,指针这个功能只会让开发者头发越来越少。
因此,到了Golang的设计者们汲取C语言的教训, 把指针的功能设置得很克制,并且大多数开发者使用到仅仅是取地址和解引用等功能。
阅读本文前,请确保你能够理解:
- 什么是指针?
- 什么是取地址?
- 什么是解引用?
unsafe
包如其名, 官方给起这个名字就是说“这玩意太危险, 不要随便用”。虽然看着不太安全,官方还是向开发者提供了这一功能,并且起了个不太吉利的名字,颇有点甩锅的意味。
unsafe
包中可导出东西不多,我们来快速过一下本文需要使用到的类型和函数。
ArbitraryType
ArbitraryType
是int类型的别名, 我们知道在不同的平台下int
类型大小会随之改变,如在x86
中int类型为32位, 在x64
平台中为64位, 会出现在文档中仅仅是为了起到说明作用。
1 | go复制代码// ArbitraryType is here for the purposes of documentation only and is not actually |
Pointer
Pointer
的实际类型是*int
, Go官方将其当作C语言中的无类型指针void*
来使用, 其类型声明如下
1 | go复制代码type Pointer *ArbitraryType |
在官方文档中我们可以找到如下解释
- 任何类型的指针都可以转换成
Pointer
Pointer
可以转换成任意类型的指针uintptr
可以转换成Pointer
Pointer
可以转换成uintptr
uintptr
是一个足够存储系统中所有内存地址的整型(x86中为32位, x64中为64位), 可以用于运算。
Go在GC的时候不会将其uintptr
当做指针(不会给目标对象加引用)。
Sizeof
Sizeof
方法会返回变量对应类型所占用的字节数。
1 | go复制代码func TestSizeof(t *testing.T) { |
输出如下所示:
切片的结构
Go中的切片是官方提供的可以动态扩容的数组,其本质是编译器的提供的语法糖,在运行时其结构体如下所示:
1 | go复制代码type SliceHeader struct { |
关于切片的更多细节,可以阅读Go语言设计与实现-切片, 写得非常棒, 十分推荐
Data
指向了一块连续的内存区域用于存储切片的元素Len
用于记录切片大小Cap
用于记录切片的容量
以上三个字段在内存中是连续存储的, 这是平台无关性的,因此我们可以通过指针运算来获取切片的大小和容量。
实战
热身!通过指针运算获取数组元素
Tip: 在golang中无法直接对指针进行运算,我们需要将其转换为uintptr
类型
以下代码的基本逻辑就是获取数组首元素的指针*int
然后将其转换为unsafe.Pointer
类型, 再通过unsafe.Sizeof
获取对应类型的大小, 最后二者相加便获得了下一个数组元素的位置,并将其转换为unsafe.Pointer
类型。
由于unsafe.Pointer
可以转换为任意类型的指针, 我们便将其转换*int
指针, 并进行解引用获取对应值。
1 | go复制代码func TestArray(t *testing.T) { |
最后输出如下所示:
观察输出结果我们可以发现array
存储的时指向首元素的地址,并且是个指针类型。
根据切片的结构定义我们可以确定array
的地址的就是SliceHeader
结构的首地址。
定位Len和Cap字段
由于结构体在内存中是连续存储的,因此在x64平台下有:
Len
字段为SliceHeader
结构的首地址 + 8Cap
字段为SliceHeader
结构的首地址 + 16
在上文中的测试用例中补充如下代码:
1 | go复制代码// 取 DataPtr 的地址, 然后我们可以根据结构体的定义算出Len字段和Cap字段的地址 |
以上代码的出发点就是对array
(指针)进行取地址获得SliceHeader
的首地址并进行运算。
输出结果如下所示:
总结
- 指针运算虽然风骚,在普通业务开发最好别用,会被打!
unsafe.Pointer
类似于C语言的中无类型指针void*
- 想要指针运算需要将其转换
uintptr
最后附上指针类型转换图:
本文转载自: 掘金