Analysis of unsafe.Pointer in Go language

You have to work really hard to look effortless!

WeChat search public number [Long Coding Road], together From Zero To Hero!

foreword

In Gothe , we will inevitably use pointers, but in most cases, we use 类型安全pointers. Type-safe pointers help us write safe code, but there are many restrictions, such as the inability to perform arithmetic operations on addresses , does not support the mutual conversion of any two types, etc.

GoIn fact, pointers are supported 非类型安全. With non-type-safe pointers, we can bypass many restrictions and in some cases even write more efficient code, but at the same time may introduce some potential problems that are not easy to find. Second, non-type-safe pointers are not protected by the Go1 compatibility guarantee , and code that uses non-type-safe pointers may fail to compile in subsequent Go versions.

Even with the above risks, non-type-safe pointers are currently used in many places in the source code, and the official way of using them is given. Let's learn together in this article!

Note: The examples in this article are all based on Go1.17 64-bit machines

type safe pointer

how to get a pointer

We have two ways to get type-safe pointers:

  1. newGet a pointer to a value of a certain type through a built-in function
  2. &Get a pointer to a variable by taking the address character
func main() {
	// 通过 new 为int类型的值开辟一块内存,并返回指向内存起始地址的指针
	a := new(int)
	fmt.Printf("%p\n", a) // 0xc00034a4b8

	// 通过取地址符 & ,获取一个变量的指针
	b := int32(1)
	c := &b
	fmt.Printf("%p\n", c) //0xc00034a4c0
}
复制代码

Why do you need to use pointers

GoIn , all parameter passing is by value, not by reference.

  1. If the parameter takes up too much memory, the variable copy is required every time the function is passed, which consumes more memory;
  2. If we want to modify the state of a variable inside a function and see the modification after the call, we need to use a pointer.

比如我们想要调用 add 完成变量的加一操作,但是最终并没有达到期望的效果,原因就是值传递,即调用 add(b) 的时候,传入的参数是 变量b 的一份复制,并不会影响 main函数变量b 本身。

func add(a int) {
	a = a + 1
}

func main() {
	b := 1
	add(b)
	println(b) // 1
}
复制代码

如果想要达到修改成功的目的,就需要传递指针:

func add(a *int) {
	*a = *a + 1
}

func main() {
	a := 1
	add(&a)
	println(a) // 2
}
复制代码

类型安全指针的限制

  1. 不能对指针的地址进行算术运算

我们定义一个变量 a ,然后取地址,对地址算数运算 addr++ 会编译不通过;*addr++ 编译通过,最后输出 a=2,其实 *addr++ 被编译器解释为了(*addr)++,即解引用操作符 * 的优先级 高于 自增符++

func main() {
	a := 1
	addr := &a
	// addr++  编译不通过
	*addr++ // 编译通过
	fmt.Println(a) // 2
}  
复制代码
  1. 两个任意指针类型不能随意转换

只有两个类型的底层数据类型是一致的,才可以完成转换

type MyInt int64
type T1 *int64
type T2 *MyInt

func main() {

	var a *int64
	var myInt *MyInt

	var t1 T1
	t1 = a // t1 是 *int64类型,a 是 *int64 类型,可以隐式转换

	var t2 T2
	t2 = myInt       // t2 是 *MyInt类型,myInt 是 *MyInt类型,可以隐式转换
	t2 = (*MyInt)(a) // t2 的底层类型是 *int64,a 是 *int64 类型,需要显式转换

	t1 = (*int64)((*MyInt)(t2)) // t2 的底层类型是 *int64,t1 是 *int64类型,需要显式转换
}
复制代码

但是这些类型,无论怎么转换,都转换不了 *uint64 类型

unsafe包

我们说的 非类型安全指针 就是指 unsafe 包中的 Pointer,它被类型定义为 type Pointer *ArbitraryTypeArbitraryType 在这里仅仅是用于表示任意类型,也就是说 Pointer 可以指向任意数据类型,可以和任意类型的指针相互转换。

// 表示任意类型
type ArbitraryType int

type Pointer *ArbitraryType
复制代码

在上篇文章中Go语言内存对齐详解,我们也简单了解了 unsafe 包中有如下三个函数:

  1. func Sizeof(x ArbitraryType) uintptr

    返回一个变量占用的内存字节数

  2. func Offsetof(x ArbitraryType) uintptr

    返回结构体某个字段的地址相对于此结构体起始地址的偏移量

  3. func Alignof(x ArbitraryType) uintptr

    返回对齐系数

这三个函数的返回值的类型均为内置类型 uintptruintptr 是一个整数值,来保存变量的内存地址,可以和 Pointer 相互转换。

Pointer 表示指向任意类型的指针,对于该类型有四种合法的操作:

  • 任意类型的指针可以转为 Pointer
  • Pointer 可以转为任意类型的指针
  • uintptr 可以转为 Pointer
  • Pointer 可以转为 uintptr
func main() {

	a := int(1)

	b := (*int64)(unsafe.Pointer(&a)) // 将 *int 先转为 Pointer,再转为 *int64

	c := uintptr(unsafe.Pointer(&a)) // 将 *int 先转为 Pointer,再转为 uintptr

	fmt.Printf("%p\n", b) // 打印地址 0xc0003cdbb0
	fmt.Printf("%x\n", c) // 地址 c0002124b8

  
	type T struct {
		a string
		b int
	}
	t := T{a: "abc", b: 1}

	/*
		1. 将 t 的地址转为 Pointer:符合第一种
		2. 将 Pointer 转为 uintptr 后得到地址的整数值:符合第四种
		3. 加上 t.b 的offset,得到 t.b 的地址整数值:uintptr是整数,可以直接相加
		4. 将 uintptr 转为 Pointer:符合第三种
		5. 将 Pointer 转为 *int :符合第二种
		6. 最后解引用,得到具体的值
	*/
	d := *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&t)) + unsafe.Offsetof(t.b)))
	fmt.Println(d) // 1
}  
复制代码

Pointer 越过了类型检查,可以直接操作底层的内存,因此使用时需要格外小心。对于 Pointer的操作,只有如下六种是合法的,其余的使用方式均为非法,我们一起来看下。

正确使用非类型安全指针

使用方式一:利用 Pointer 作为中介,完成 T1 类型 到 T2 类型的转换

T1T2 是任意类型,如果 T1 的内存占用大于等于 T2,并且 T1 和 T2 的内存布局一致,可以利用 Pointer 作为中介,完成 T1类型 到 T2类型的转换。(如果T1 的内存占用小于 T2,那么 T2 剩余部分没法赋值,就会有问题)

math 包中的 Float64bits 函数将一个 float64 值转换为一个 uint64 值,Float64frombits 为此转换的逆转换,即 Float64bits(Float64frombits(x)) == x。

func Float64bits(f float64) uint64 {
	return *(*uint64)(unsafe.Pointer(&f)) 
}

func Float64frombits(b uint64) float64 {
	return *(*float64)(unsafe.Pointer(&b)) 
}
复制代码

如下所示,slicestring 结构的底层布局类似,且 slice 的内存占用大于 string,我们可以利用此种方式完成 slice 到 string 的正确转换,但是无法正确完成 string 到 slice 的转换。

// slice 和 string 的底层结构
type slice struct {
	array unsafe.Pointer
	len   int
	cap   int
}

type stringStruct struct {
	str unsafe.Pointer
	len int
}
复制代码
func main() {

	// slice 转 string,可以正确转换
	sli := []byte{'a', 'b', 'c'}
	str := *(*string)(unsafe.Pointer(&sli))
	fmt.Println(str)      // abc
	fmt.Println(len(str)) // 3

	// string 转 slice,cap 字段无法赋值,无法正确转换
	str = "1234"
	b := *(*[]byte)(unsafe.Pointer(&str))
	fmt.Println(string(b)) // 1234
	fmt.Println(len(b))    // 4
	fmt.Println(cap(b))    // 824634066744
}  
  
复制代码

slice 转为 string 后,两者对应的指针指向的是同一个字节数组,因此修改底层的数组值,string 相应的也会跟着改变。

func main() {

	// 字节数组转字符串
	sli := []byte{'a', 'b', 'c'}
	str := *(*string)(unsafe.Pointer(&sli))
	fmt.Println(str)      // abc
	fmt.Println(len(str)) // 3

	sli[0] = 'd'
	sli[1] = 'e'
	fmt.Println(str) // dec
}  
复制代码

使用方式二:将 Pointer 转为 uintptr (不再转回 Pointer)

Pointer 转为 uintptr,并且不再转回 Pointer,此方式用处不大,通常我们只用来打印值。

此方式相当于取变量的内存地址,由于 uintptr 是个变量值,而非引用,后续该变量被移动到其他位置,其对应的uintptr值不会更新;其次,如果后续没有使用该变量,随时可能会被垃圾回收掉。

// 每次运行得到的内存地址,可能不一样
func main() {
	a := int(10)
	fmt.Printf("%p\n", &a)                          // 0xc0001184b8
	fmt.Printf("%x\n", uintptr(unsafe.Pointer(&a))) // c0001184b8
}  
复制代码

因此,将 uintptr 转回 Pointer 是存在风险的,只有接下来我们列举的几种转换方式合法的。

使用方式三:将Pointer转为 uintptr,然后再通过算数方式将 uintptr 转回 Pointer

我们可以将一个变量的 Pointer 转为 uintptr,然后再加上一定的偏移量转回 Pointer,这种方式通常用来获取结构体中的成员变量地址或者数组中第i个元素的地址。

结构体:我们可以先拿到结构体变量 e 的地址,然后加上 成员b 的偏移量,就可以得到 e.b 的地址,再转回 Pointer 就能够拿到对应的值了。

func main() {

	type Example struct {
		a int32
		b string
	}

	e := Example{
		a: 1,
		b: "test",
	}

	// 等价于 *(*string)(unsafe.Pointer(&e.b))
	c := *(*string)(unsafe.Pointer(uintptr(unsafe.Pointer(&e)) + unsafe.Offsetof(e.b)))

	fmt.Println(c, d)
}  
复制代码

数组:拿到了数组第一个元素 a[0] 的地址,转为 uintptr 后,加上 2倍 个元素类型占用的内存大小,就可以得到第 3 个元素的地址值,再转回 Pointer,最后转为 int,就得到了第三个元素的值。

func main() {
	a := []int{1, 2, 3, 4}
	b := *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&a[0])) + 2*unsafe.Sizeof(a[0])))
	fmt.Println(b)
}  
复制代码

同理,获取一个成员或元素的地址,然后减去相应的偏移量,也是合法操作。但是无论怎么操作,需要保证最后得到的地址,是在当前变量占用的地址范围内,不能超出,如下几种就是非法的操作:

  • 非法操作一:超出变量内存范围
// 从初始地址,最多加  unsafe.Sizeof(s)-1
var s thing
end = unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + unsafe.Sizeof(s))
复制代码
// 声明了 n 个字节的长度,从初始地址最多加 n-1
b := make([]byte, n)
end = unsafe.Pointer(uintptr(unsafe.Pointer(&b[0])) + uintptr(n))
复制代码
  • 非法操作二:使用变量保存 uintptr 的值

在将 uintptr 类型转为 Pointer 类型之前,不能将 uintptr 的的值赋值给变量

// 非法操作示例
func main() {

	type Example struct {
		a int32
		b string
	}

	e := Example{
		a: 1,
		b: "test",
	}

	// 正确操作 c := *(*string)(unsafe.Pointer(uintptr(unsafe.Pointer(&e)) + unsafe.Offsetof(e.b)))
	addr := uintptr(unsafe.Pointer(&e)) + unsafe.Offsetof(e.b)

	// 到这里,变量 e 没有任何引用了,因此可能随时被垃圾回收器回收,一旦被回收,再使用 e.b 原来的地址将是非常危险的
	c := *(*string)(unsafe.Pointer(addr))

	fmt.Println(c)
」	
复制代码
  • 非法操作三:Pointer 指向 nil

Pointer 需要指向一个分配过内存的变量,不能指向 nil

// Pintere指向nil是非法的
u := unsafe.Pointer(nil)
p := unsafe.Pointer(uintptr(u) + offset)
复制代码

使用方式四:将 Pointer 转为 uintptr, 传递给系统调用 syscall.Syscall

我们知道 uintptr 是一个整数,获取到了一个变量的 uintptr 值,并不能保证变量不被垃圾回收掉,如果变量被垃圾回收掉,使用原先的 uintptr 值将是非常危险的。

下面这个函数是危险的原因在于,函数本身不能保证传递进来的地址对应的内存块一定没有被回收。 如果此内存块已经被回收了或者被重新分配给了其它变量,那么此函数内部的操作将是非法和危险的。

func DoSomething(addr uintptr) {
    // 对处于传递进来的地址处的值进行读写...
}
复制代码

然而系统调用则有这种特权,保证了地址对应的内存块在函数执行过程中不被回收和移动。例如 syscall 标准库包中的 Syscall 函数的原型为:

func Syscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err Errno)
复制代码

那么此函数是如何保证传递给它的地址参数值a1a2a3处的内存块在执行过程中一定没有被回收和被移动呢? 此函数无法做出这样的保证,事实上,是编译器做出了这样的保证。 这是 syscall.Syscall 这样函数的特权,其它自定义函数无法享受到这样的待遇。

正确的使用姿势为:

// 将 p 对应的 Pointer 值转为 uintptr
syscall.Syscall(SYS_READ, uintptr(fd), uintptr(unsafe.Pointer(p)), uintptr(n))
复制代码

同时需要注意的是,我们也不能先将 uintptr 的值赋值给一个变量,然后再传入 syscall.Syscall

u := uintptr(unsafe.Pointer(p))
// 此时 p 可能被回收或者移动
syscall.Syscall(SYS_READ, uintptr(fd), u, uintptr(n))
复制代码

使用方式五:将 reflect.Value.Pointer 或者 reflect.Value.UnsafeAddruintptr 值转为 unsafe.Pointer

reflect包中,Value 类型的 Pointer UnsafeAddr 方法都返回一个 uintptr 值,而不是 unsafe.Pointer 值,这样做是为了避免用户在没有引入 unsafe 包的条件下,就可以将这两个方法的返回值转为任意类型安全的指针。(比如返回值 a 是 unsafe.Pointer 类型,不引入unsafe包,可以直接进行(*int32)(a),将其转为 int32 类型的指针 )。

因此,这种设计需要我们在调用完 reflect.Value.Pointer 或者 reflect.Value.UnsafeAddr后,立即调用 unsafe.Pointer 转为 Pointer 类型,否则在调用的空窗期,变量可能被移动或者回收。

func main() {

	type Example struct {
		a int32
		b string
	}

	e := Example{
		a: 1,
		b: "test",
	}

	// 1. 正确使用方式
	b := *(*string)(unsafe.Pointer(reflect.ValueOf(&e.b).Pointer()))
	fmt.Println(b) // test

  // 2. 错误使用方式
	p := reflect.ValueOf(&e.b).Pointer()
	// 此时变量可能被移动或者回收
	b = *(*string)(unsafe.Pointer(p))
	fmt.Println(b) 
}  
复制代码

使用方式六:将 reflect.SliceHeader 或者 reflect.StringHeaderData 域对应的 uintptr 转为 Pointer,或者将其他 Pointer 转为 uintptr 赋值给 Data

slicestring 底层的数据结构如下:其中 slice 结构的 array 字段和 string 结构的 str 字段底层其实都指向 字节数组

SliceHeaderStringHeader 分别是 slicestring 结构的运行时表示,对于任意一个 slice 或者 string,我们可以拿到它的运行时表示,然后修改其 Data 值,达到修改其底层数据的目的。即我们可以将一个字符串的指针值 转换为 *reflect.StringHeader ,进而可以对此字符串的内部进行修改。类似,我们也可以将一个切片的指针值转换为 *reflect.SliceHeader ,从而对此切片的内部进行修改。

这样做的好处是,在不重新分配内存的情况下,将 stringslice 的底层数据改变。

type slice struct {
	array unsafe.Pointer
	len   int
	cap   int
}

type stringStruct struct {
	str unsafe.Pointer
	len int
}

type SliceHeader struct {
	Data uintptr
	Len  int
	Cap  int
}

type StringHeader struct {
	Data uintptr
	Len  int
}
复制代码

和上面第五条同样的原因,为了避免用户没有引入 unsafe包 就可以直接转换, reflect.SliceHeader 或者 reflect.StringHeaderData 域都是 uintptr 类型。

// 修改字符串对应的Data域
func main() {

	str := "test"
  
  // 字节数组,修改后字符串底层数据指向这个数组
	a := [3]byte{'a', 'b', 'c'}

	strHeader := (*reflect.StringHeader)(unsafe.Pointer(&str))
	strHeader.Data = uintptr(unsafe.Pointer(&a))
	strHeader.Len = len(a)

	fmt.Println(str) // abc
}  
复制代码
func main() {
  
	sli := []byte{'h', 'e', 'l', 'l', 'o'}

	array := [4]byte{'1', '2', '3', '4'}

	// 将切片转为 reflect.SliceHeader 结构
	sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&sli))

	// 修改对应的字段数据,修改后 sli 底层的数据指向了 array
	sliceHeader.Data = uintptr(unsafe.Pointer(&array))
  
  // 先设置长度为2
	sliceHeader.Len = 2
	sliceHeader.Cap = len(array)
	fmt.Printf("%s\n", sli) // 12

	// 修改 sli 的长度
	sli = sli[:cap(sli)]
	fmt.Printf("%s\n", sli) // 1234
  
}
复制代码

In general, we should get from an existing string *reflect.StringHeader, or from an existing slice *reflect.SliceHeader, and cannot directly declare reflect.SliceHeaderor reflect.StringHeadervariables:

// 错误使用方式
var hdr reflect.StringHeader
hdr.Data = uintptr(unsafe.Pointer(new([5]byte)))
 // 在此时刻,上一行代码中刚开辟的数组内存块已经不再被任何值所引用,所以它可以被回收
hdr.Len = n
s := *(*string)(unsafe.Pointer(&hdr)) // 危险
复制代码

Using reflect.SliceHeaderand , we can do and type interchange withoutreflect.StringHeader reallocating the underlying data memory :slicestring

// 字节切片转 string
func ByteSlice2String(slice []byte) (s string) {
	sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&slice))
	stringHeader := (*reflect.StringHeader)(unsafe.Pointer(&s))
	stringHeader.Data = sliceHeader.Data
	stringHeader.Len = sliceHeader.Len
	return
}

// string 转字节切片
func String2ByteSlice(s string) (slice []byte) {
	stringHeader := (*reflect.StringHeader)(unsafe.Pointer(&s))
	sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&slice))

	sliceHeader.Data = stringHeader.Data
	sliceHeader.Len = stringHeader.Len
	sliceHeader.Cap = stringHeader.Len
	return
}

func main() {

	b := []byte{'h', 'e', 'l', 'l', 'o'}
	fmt.Println(ByteSlice2String(b)) // hello

	s := "hello"
	fmt.Println(String2ByteSlice(s)) // [104 101 108 108 111]
  
}  
  
复制代码

Since the default string memory is allocated in the unmodifiable area, after using the above to String2ByteSlice convert tostring , it can only be read, and its underlying data value cannot be modified:slice

func main() {

	s1 := "Goland" // 官方标准编译器会将 s1 的字节开辟在不可修改内存区
	
	b1 := String2ByteSlice(s1) // 转为字节数组
	fmt.Printf("%s\n", b1) // Goland

	// 由于字符串 s1 底层指向的字节数组在不可修改区,此时不能修改值,否则会panic
	// b1[5] = 'a'

	// 这种方式不会存放在不可修改区,转为字节数组后,可以修改值
	s2 := strings.Join([]string{"Go", "land"}, "")
	b2 := String2ByteSlice(s2)
	fmt.Printf("%s\n", b2) // Goland
	b2[5] = 'g' // 相当于修改底层数组的值,原字符串的值也会随之改变
	fmt.Println(s2) // Golang
}
复制代码

Summarize

This article starts with type-safe pointers, introduces how to obtain pointers, why you need to use pointers, and the limitations of type-safe pointers, then further introduces unsafethe Pointerdefinition and usage of non-type-safe pointer types in packages, and finally details through specific examples. Six scenarios for proper use Pointerare .

More

Personal blog: lifelmy.github.io/

WeChat public account: Long Coding Road

Guess you like

Origin juejin.im/post/7083853142403579911