Golang如何通过指针运算获取切片大小和容量?

「这是我参与11月更文挑战的第11天,活动详情查看:2021最后一次更文挑战

如果你有接触过C语言, 相信你会对其指针功能的强大印象深刻, 但大多数情况下,指针这个功能只会让开发者头发越来越少。

因此,到了Golang的设计者们汲取C语言的教训, 把指针的功能设置得很克制,并且大多数开发者使用到仅仅是取地址和解引用等功能。

阅读本文前,请确保你能够理解:

  • 什么是指针?
  • 什么是取地址?
  • 什么是解引用?

unsafe

包如其名, 官方给起这个名字就是说“这玩意太危险, 不要随便用”。虽然看着不太安全,官方还是向开发者提供了这一功能,并且起了个不太吉利的名字,颇有点甩锅的意味。

unsafe包中可导出东西不多,我们来快速过一下本文需要使用到的类型和函数。

ArbitraryType

ArbitraryType 是int类型的别名, 我们知道在不同的平台下int类型大小会随之改变,如在x86中int类型为32位, 在x64平台中为64位, 会出现在文档中仅仅是为了起到说明作用。

1
2
3
go复制代码// ArbitraryType is here for the purposes of documentation only and is not actually
// part of the unsafe package. It represents the type of an arbitrary Go expression.
type ArbitraryType int

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
2
3
4
5
6
7
8
9
10
11
12
13
go复制代码func TestSizeof(t *testing.T) {
b := byte(0xFF)
t.Logf("Size of byte %d", unsafe.Sizeof(b))
type Msg struct{
Id int64
Message string
}
msg := Msg{}
t.Logf("Sizeof msg %d", unsafe.Sizeof(msg))
t.Logf("Sizeof msg ptr %d", unsafe.Sizeof(&msg))
slice := make([]int, 0)
t.Logf("Sizeof slice %d", unsafe.Sizeof(slice))
}

输出如下所示:

image.png

切片的结构

Go中的切片是官方提供的可以动态扩容的数组,其本质是编译器的提供的语法糖,在运行时其结构体如下所示:

1
2
3
4
5
go复制代码type SliceHeader struct {
Data uintptr
Len int
Cap int
}

关于切片的更多细节,可以阅读Go语言设计与实现-切片, 写得非常棒, 十分推荐

  • Data指向了一块连续的内存区域用于存储切片的元素
  • Len 用于记录切片大小
  • Cap 用于记录切片的容量

以上三个字段在内存中是连续存储的, 这是平台无关性的,因此我们可以通过指针运算来获取切片的大小和容量。

实战

热身!通过指针运算获取数组元素

Tip: 在golang中无法直接对指针进行运算,我们需要将其转换为uintptr类型

以下代码的基本逻辑就是获取数组首元素的指针*int然后将其转换为unsafe.Pointer类型, 再通过unsafe.Sizeof获取对应类型的大小, 最后二者相加便获得了下一个数组元素的位置,并将其转换为unsafe.Pointer类型。

由于unsafe.Pointer可以转换为任意类型的指针, 我们便将其转换*int指针, 并进行解引用获取对应值。

1
2
3
4
5
6
7
8
9
10
11
12
go复制代码func TestArray(t *testing.T) {
array := make([]int, 0, 10)
array = append(array, 1024)
array = append(array, 7777)
firstPtr := unsafe.Pointer(&array[0])
// 指针运算
secondPtr := unsafe.Pointer(uintptr(firstPtr) + unsafe.Sizeof(array[0]))
val := (*int)(secondPtr)
t.Logf("array address %p", array)
t.Logf("array[0]=%d address=%p", array[0], firstPtr)
t.Logf("array[1]=%d address=%p", *val, secondPtr)
}

最后输出如下所示:

image.png

观察输出结果我们可以发现array存储的时指向首元素的地址,并且是个指针类型。
根据切片的结构定义我们可以确定array的地址的就是SliceHeader结构的首地址。

定位Len和Cap字段

由于结构体在内存中是连续存储的,因此在x64平台下有:

  • Len字段为SliceHeader结构的首地址 + 8
  • Cap字段为SliceHeader结构的首地址 + 16

在上文中的测试用例中补充如下代码:

1
2
3
4
5
6
7
go复制代码// 取 DataPtr 的地址, 然后我们可以根据结构体的定义算出Len字段和Cap字段的地址
dataPtr := unsafe.Pointer(&array)
t.Logf("Data ptr=%p", dataPtr)
lenPtr :=(*int)(unsafe.Pointer(uintptr(dataPtr) + 8))
capPtr :=(*int)(unsafe.Pointer(uintptr(dataPtr) + 16))
t.Logf("len ptr=%p value=%d", lenPtr, *lenPtr)
t.Logf("cap ptr=%p value=%d", capPtr, *capPtr)

以上代码的出发点就是对array(指针)进行取地址获得SliceHeader的首地址并进行运算。

输出结果如下所示:

image.png

总结

  • 指针运算虽然风骚,在普通业务开发最好别用,会被打!
  • unsafe.Pointer类似于C语言的中无类型指针void*
  • 想要指针运算需要将其转换uintptr

最后附上指针类型转换图:

image.png

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%