likes
comments
collection
share

8. 看 Go 源码,你需要了解 unsafe.Pointer

作者站长头像
站长
· 阅读数 19

前言

// 获取下一个溢出桶
func (b *bmap) overflow(t *maptype) *bmap {
    return *(**bmap)(add(unsafe.Pointer(b), uintptr(t.bucketsize)-sys.PtrSize))
}

上边这段代码利用 unsafe.Pointeruintptr 类型的特性,通过直接对内存地址进行操作的方式,实现了链表下一节点的查找,使得代码更加高效。当然有利自然有弊,它实际上绕开了 Go 语言的类型安全机制,通过它可以直接操作内存,这种行为无疑是危险的,就如 unsafe 包的命名一样,官方都认为这个类型是很不安全的,所以轻易不要使用,但我们要解它,才能更好的阅读源码。

1.Go 中的指针

因为 unsafe.Pointer 与指针息息相关,因此我们先简单来了解一下 Go 语言中的指针。

1.1 指针的基本使用

变量的本质是对一块内存空间的命名,我们可以通过引用变量名来使用这块内存空间存储的值,而指针则是用来指向这些变量值所在内存地址的值

Go 语言支持指针,如果一个变量是指针类型的,那么就可以用这个变量来存储指针类型的值。简单举个例子:

package main

import "fmt"

func main() {
	var x *int
	a := 1
	x = &a
	fmt.Println("a:", a, ",", "x:", x)
	a++
	fmt.Println("&a:", &a, ",", "*x:", *x)
	*x++
	fmt.Println("a:", a, ",", "*x:", *x)
	x = nil
	fmt.Println("a:", a, ",", "x:", x)
}

// 输出结果
a: 1 , x: 0xc000120000
&a: 0xc000120000 , *x: 2
a: 3 , *x: 3
a: 3 , x: <nil>

我们解释一下上边的代码,了解一下指针的基本使用:

&a 表示取变量 a 的地址,也就是 0xc000120000

x变量是指针变量,指向了变量 a 所在的内存地址 0xc000120000

*x表示解引用得到内存地址存储的变量 a,此时 *x 等价于 a,因此变量 a 的修改会影响 *x,相反 *x 的修改也会反应到变量 a 上;但 x = nil 改变了指针变量 x 的指向,只是断开了与变量 a 的引用关系,因此并不能影响变量 a

1.2 指针作为函数参数

指针变量常被用作参数传递,不仅可以节省内存空间,还可以在调用函数中实现对变量值的修改。 举个例子对比一下 Go 语言中的参数传递:

package main

import "fmt"

func swap1(a, b int) {
	a, b = b, a
}

func swap2(a, b *int) {
	a, b = b, a
}

func swap3(a, b *int) {
	*a, *b = *b, *a
}

func main() {
	a := 1
	b := 2
	swap1(a, b)
	fmt.Println(a, b)
	swap2(&a, &b)
	fmt.Println(a, b)
	swap3(&a, &b)
	fmt.Println(a, b)
}

我们都知道 Go 中函数参数为“值传递”,也就意味着传递到函数 swap 的都是 a,b 变量的副本:

  • 由于 swap1 传递的是变量值的副本,因此在函数内修改不会影响原参数;
  • swap2 传递的是指针变量的副本,但并没有对指针变量所指向的内存地址做更改,只是更改了副本指针变量的指向地址,因此也不会影响原参数;
  • swap3 传递的也是指针变量的副本,但通过解引用的方式,修改了指针变量指向的内存地址所代表的值,因此影响了原参数,达到了函数最终的目的,指针的作用也由此体现。

因此,只有通过指针变量修改了所指向的内存地址中存储的值时,才会对原变量产生影响;如果只是对变量的副本做改变,是不会影响原变量的。

1.3 指针的限制

Go 既然有指针了,为什么还需要 unsafe.Pointer 类型呢?

这就得聊一聊 Go 语言中对指针的一些限制了:

  1. Go 中指针不能进行算术运算。例如:&a++
  2. Go 中不同类型的指针不能相互转换。例如:var a int = 1;f := (*float64)(&a)
  3. Go 中不同类型的指针不能比较和相互赋值,例如:var a int = 1;var f float64;f = &a;&a == &f

以上指针错误的使用方式在 Go 中都会编译报错。

Go 语言是一门强类型的静态语言,意味着类型一旦定义就不能改变,为了安全考虑,对指针做了以上限制。然而,为了性能的高效,官方开放了一个指针类型 unsafe.Pointer,它可以包含任意类型变量的地址,它绕开了 Go 语言的类型系统,通过它可以直接操作内存,因此使用起来并不安全,接下来我们就一起看看 unsafe.Pointer 是什么,以及用它都玩些什么花活。

2.unsafe.Pointer 是什么

unsafe.Pointer 是特别定义的一种指针类型,它可以包含任意类型变量的地址。

代码地址:src/unsafe/unsafe.go,源码中只有 5 行代码,其余都是注释。

type ArbitraryType int
type Pointer *ArbitraryType

func Sizeof(x ArbitraryType) uintptr
func Offsetof(x ArbitraryType) uintptr
func Alignof(x ArbitraryType) uintptr

ArbitraryTypeint 的一个别名,意思为任意的类型; Pointer*ArbitraryType 的一个别名,表示指向任意类型的指针。 Go 官方文档对这个类型有如下四个描述:

  1. 任何类型的指针都可以被转化为 unsafe.Pointer
  2. unsafe.Pointer 可以被转化为任何类型的指针;
  3. uintptr 可以被转化为 unsafe.Pointer
  4. unsafe.Pointer 可以被转化为 uintptr

什么是 uintptr

源码地址:src/builtin/builtin.go

// uintptr is an integer type that is large enough to hold the bit pattern of
// any pointer.
type uintptr uintptr

uintptr 是一个整数类型,足够大能保存任何一种指针类型。uintptr 指的是具体的内存地址,不是个指针,因此 uintptr 地址关联的对象可以被垃圾回收。而 unsafe.Pointer 有指针语义,可以保护它不会被垃圾回收。

指针类型、unsafe.Pointer、uintptr 三者关系如下:

指针类型 *T <-> unsafe.Pointer <-> uintptr

uintptr 是可用于存储指针的整型,而整型是可以进行数学运算的。因此,将 unsafe.Pointer 转化为 uintptr 类型后,就可以让本不具备运算能力的指针具备了指针运算能力。

unsafe 包中还有三个函数,这里简单解释一下:

  • Sizeof:返回类型 x 所占据的字节数,但不包含 x 所指向的内容的大小。例如,如果 x 是切片,则 Sizeof 返回切片描述符的大小,而不是切片引用的内存的大小。
  • Offsetof:返回 x 表示的字段在结构体中的偏移量,该字段必须采用 structValue.field 形式。 换句话说,它返回结构体开头和字段开头之间的字节数。
  • Alignof:返回变量 x 所需的对齐方式,例如;如果变量 s 是结构体类型,并且 f 是该结构体中的字段,则 Alignof(s.f) 将返回结构体中该类型字段所需的对齐方式。

总结:

  1. unsafe.Pointer可以和任意的指针类型进行转换,意味着可以借助 unsafe.Pointer 完成不同指针类型之间的转换。
  2. unsafe.Pointer 可以转换为 uintptr,而 uintptr 拥有计算能力,因此指针可以借助 unsafe.Pointeruintptr 完成算术运算,进而直接操作内存。

3.unsafe.Pointer 实战

了解了 unsafe.Pointer 是什么后,我们通过实际的例子对其加深一下印象。

3.1 指针类型转换

unsafe.Pointer 可以在不同的指针类型之间做转化,从而可以表示任意可寻址的指针类型,利用 unsafe.Pointer 为中介,即可完成指针类型的转换。 先举一个简单例子,看一下不同指针类型之间的转换过程:

package main

import (
	"fmt"
	"unsafe"
)

func main() {
	i := 100
	intI := &i
	var floatI *float64
	floatI = (*float64)(unsafe.Pointer(intI))
	*floatI = *floatI * 3
	fmt.Printf("%T\n", i)
	fmt.Println(i)
	fmt.Printf("%T\n", intI)
	fmt.Printf("%T\n", floatI)
}

// 输出
int
300
*int
*float64

该例子中定义了两个指针变量分别是 *int 类型的 intI*float64 类型的 floatI,然后先对 intI 做了类型 unsafe.Pointer 的转换,随后进行 *float64 类型的转换;然后对 *float64 进行乘法操作,最终影响到了 i 变量,也从侧面证明了 *float64 的指针变量是指向 i 变量的内存地址的。

然后我们看一个类型转换经典的例子:实现字符串和 bytes 切片之间的转换,要求是零拷贝。 如果按照以往的方式,循环遍历,然后挨个拷贝赋值是无法完成目标的,这个时候只能考虑共享底层 []byte 数组才可以实现零拷贝转换。 string 和 []byte 在运行时的类型表示为 reflect.StringHeaderreflect.SliceHeader

源码:src/reflect/value.go

type StringHeader struct {
	Data uintptr
	Len  int
}

type SliceHeader struct {
	Data uintptr
	Len  int
	Cap  int
}

使用 unsafe.Pointerstring[]byte 转换为 *reflect.StringHeader*reflect.SliceHeader,然后通过构造方式,完成底层 []byte 数组的共享,最后通过指针类型转换方式再次转换回来,代码如下:

func string2bytes(s string) []byte {
	stringHeader := (*reflect.StringHeader)(unsafe.Pointer(&s))

	bh := reflect.SliceHeader{
		Data: stringHeader.Data,
		Len:  stringHeader.Len,
		Cap:  stringHeader.Len,
	}

	return *(*[]byte)(unsafe.Pointer(&bh))
}

func bytes2string(b []byte) string{
	sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&b))

	sh := reflect.StringHeader{
		Data: sliceHeader.Data,
		Len:  sliceHeader.Len,
	}

	return *(*string)(unsafe.Pointer(&sh))
}

3.2 指针运算

通过上边的分析,我们知道指针可以借助 unsafe.Pointeruintptr 完成指针运算,接下来我们看两个例子。

案例一:通过指针运算,修改数组内部的值。

package main

import (
	"fmt"
	"unsafe"
)

func main() {
	arr := [3]int{1, 2, 3}
	ap := &arr
	arr0p := (*int)(unsafe.Pointer(ap))
	arr1p := (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(ap)) + unsafe.Sizeof(arr[0])))
	*arr0p += 10
	*arr1p += 20
	fmt.Println(arr) // [11 22 3]
}

解释一下关键代码: arr0p := (*int)(unsafe.Pointer(ap)) 通过类型转换的方式,使得 arr0p 变量指向了 arr 数组的起始地址,也就是 arr[0] 所在的起始地址;

uintptr(unsafe.Pointer(ap))将数组的起始地址转换为 uintptr 类型,然后加上unsafe.Sizeof(arr[0])数组第一个元素的偏移量,得到 arr[1] 的起始地址,通过类型转换为 *int 指针,赋值给 arr1p。最终做到通过指针修改数组内部元素的值。

案例二:通过指针运算,修改结构体内的值。

package main

import (
	"fmt"
	"unsafe"
)

type user struct {
	name string
	age  int
}

func main() {
	u := new(user)
	fmt.Println(*u) // { 0}

	pName := (*string)(unsafe.Pointer(u))
	*pName = "张三"

	pAge := (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(u)) + unsafe.Offsetof(u.age)))
	*pAge = 20

	fmt.Println(*u) // {张三 20}
}

通过 unsafe.Offsetof(u.age) 内存偏移的方式,定位到我们需要操作的字段(比如: age),然后改变它们的值。具体的类型转换过程和案例一类似,这里不再赘述。

4.总结

unsafe.Pointer 有两个最重要的作用:

  1. 作为不同类型指针互相转换的中介;
  2. 利用 uintptr 突破指针不能进行算术运算的限制,从而达到直接操作内存的目的。

unsafe.Pointer 类型可以绕开 Go 语言的类型系统,还可以直接操作内存,方便了很多代码的编写,且提升了代码性能,比如:底层类型相同的指针之间的转换,访问结构体私有字段等。但同时因为其特性,使其变得很不安全,因此请各位慎用。

以上就是本文的全部内容,如果觉得还不错的话欢迎点赞转发关注,感谢支持。

转载自:https://juejin.cn/post/7298645450554605568
评论
请登录