【Golang】unsafe.Pointer和uintptr

218 阅读4分钟

学习目标

  1. 是什么

  2. 有什么用

  3. 如何用

  4. 常见应用场景

简介

unsafe.Pointeruintptr从命名上看就知道和指针有关系

unsafe.Pointer

是什么

指针类型,是任意类型指针,可以指向任意类型,不像普通指针类型*T只能指向T类型。

源码:

type Pointer *ArbitraryType
type ArbitraryType int

官方文档描述:

Pointer represents a pointer to an arbitrary type. There are four special operations available for type Pointer that are not available for other types:

  • A pointer value of any type can be converted to a Pointer.

  • A Pointer can be converted to a pointer value of any type.

  • A uintptr can be converted to a Pointer.

  • A Pointer can be converted to a uintptr.

如图:

即,unsafe.Pointer作为“桥梁

如何使用

如图:

官方文档描述:

The following patterns involving Pointer are valid.

Code not using these patterns is likely to be invalid today or to become invalid in the future.

Even the valid patterns below come with important caveats.

Running "go vet" can help find uses of Pointer that do not conform to these patterns, but silence from "go vet" is not a guarantee that the code is valid.

正确的使用方式有(来自官方文档,可看源码介绍):

  1. Conversion of a *T1 to Pointer to *T2.
  2. Conversion of a Pointer to a uintptr (but not back to Pointer).
  3. Conversion of a Pointer to a uintptr and back, with arithmetic.
  4. Conversion of a Pointer to a uintptr when calling syscall.Syscall.
  5. Conversion of the result of reflect.Value.Pointer or reflect.Value.UnsafeAddr from uintptr to Pointer.
  6. Conversion of a reflect.SliceHeader or reflect.StringHeader Data field to or from Pointer.

具体case:

实现不同类型指针的互转,如下:

func main() {
   i := 3
   iPtr := &i

   j := (*int64)(unsafe.Pointer(iPtr))
   fmt.Println(iPtr, j) // iPtr、j共用同一个指针,都指向i变量
   fmt.Printf("%T %d\n", j, *j)

   *j = 8
   fmt.Println(i, *iPtr, *j)
}

输出:

0xc00001c088 0xc00001c088
*int64 3
8 8 8

uintptr

是什么

是Go内置类型,本质上就是整型,存储无符号整数可存储指针值(即内存地址) ,源码:

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

常用于指针运算

怎么使用

实现指针运算:将普通指针类型*T转成unsafe.Pointer再转成uintptr,然后做加减法,再转回unsafe.Pointer,再转回普通指针类型,之后通过*操作符就能读/写指针指向的值了。


具体case:

通过指针偏移修改结构体成员,如下:

type Student struct {
   Name    string
   Address string
}

func main() {
   stu := Student{
      Name:    "张三",
      Address: "aaa",
   }

   stuPtr := &stu
   // 可以看到,「结构体的地址」和「结构体第一个成员的内存地址」是相等的
   fmt.Printf("%p %p\n", stuPtr, &stuPtr.Name)

   namePtr := (*string)(unsafe.Pointer(stuPtr))
   fmt.Println("namePtr:", namePtr, ", value:", *namePtr)

   *namePtr = "李四"
   fmt.Println("stu:", stu)

   // 转成uintptr进行指针运算,然后再转成任意类型指针Pointer,再转成具体类型指针*string
   addressPtr := (*string)(unsafe.Pointer((uintptr)(unsafe.Pointer(stuPtr)) + unsafe.Offsetof(stuPtr.Address)))
   fmt.Println(*addressPtr)

   *addressPtr = "bbb"
   fmt.Println("stu:", stu)
}

输出结果:

0xc0000b4000 0xc0000b4000
namePtr: 0xc0000b4000 , value: 张三
stu: {李四 aaa}
aaa
stu: {李四 bbb}

再看一个case:

func main() {
   byts := []byte{1, 2, 3, 4}
   ptr1 := uintptr(unsafe.Pointer(&byts[0]))
   ptr2 := uintptr(unsafe.Pointer(&byts[1]))
   ptr3 := uintptr(unsafe.Pointer(&byts[2]))
   ptr4 := uintptr(unsafe.Pointer(&byts[3]))
   // 输出各内存地址值,正好是连续的内存地址
   fmt.Println(ptr1)
   fmt.Println(ptr2)
   fmt.Println(ptr3)
   fmt.Println(ptr4)

   i0 := (*byte)(unsafe.Pointer(ptr1))
   i1 := (*byte)(unsafe.Pointer(ptr1 + 1)) // 内存地址加1个字节
   i2 := (*byte)(unsafe.Pointer(ptr1 + 2)) // 内存地址加2个字节
   i3 := (*byte)(unsafe.Pointer(ptr1 + 3)) // 内存地址加3个字节
   fmt.Println(*i0, *i1, *i2, *i3)

   var num int32 = 511 // 511的二进制:0000.....111111111,即23个0+9个1
   // 转成*byte,所以仅访问一个字节的数据,即11111111
   tmp := (*byte)(unsafe.Pointer(&num))
   fmt.Println(*tmp)                    // 输出结果:255
   // 内存地址加1个字节,再访问1个字节的数据,即00000001
   tmp = (*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(&num)) + 1))
   fmt.Println(*tmp) // 输出结果:1
}

输出结果:

824634330796
824634330797
824634330798
824634330799
1 2 3 4
255
1

unsafe.Pointer和uintptr的区别

本质是不同的

  1. unsafe.Pointer:本质是指针类型

  2. uintptr:本质是整型,只是存储「指针值」(即内存地址

总结:指针和指针的具体类型

指针,就是一个内存地址

指针的具体类型,决定了该指针指向多大的内存区域,访问指针就是访问这块内存区域的数据。

具体case如下:

func main() {
   var num int32 = 511 // 511的二进制:0000.....111111111,即23个0+9个1
   // 转成*byte,所以仅访问一个字节的数据,即11111111
   tmp := (*byte)(unsafe.Pointer(&num))
   fmt.Println(*tmp)                    // 输出结果:255
   // 内存地址加1个字节,再访问1个字节的数据,即00000001
   tmp = (*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(&num)) + 1))
   fmt.Println(*tmp) // 输出结果:1
}

输出结果:

255
1