Go 的三种指针

Go 的三种指针

在 Go 语言中,存在着三种指针:类型安全指针、unsafe.Pointer 和 uintptr 。在日常开发中,经常使用的就是类型安全指针。但是在一些特殊场景下,需要用到其他两种指针才能实现需求。

三种指针间可以进行相互转换,其转换关系为:

类型安全指针
unsafe.Pointer
uintptr

一、类型安全指针

类型安全指针是在日常开发中最常使用的指针。类型安全指针用法与 C 语言中的类似,但是在 Go 中的类型安全指针没法像 C 语言那样对指针进行算术操作,只能进行取值和取地址、类型转换和比较。

取值和取地址:

func main() {
    p1 := new(int)   // 创建一个 *int 类型的指针
    fmt.Println(p1)  // 输出十六进制的地址
    fmt.Println(*p1) // 输出 0
}

类型转换和比较:

当两个指针类型变量的底层类型一致时,才能进行比较和显示转换

type MyInt int
type PInt *int
type PMyInt *MyInt

func main() {
    p1 := new(int)        // p1 类型为 *int
	var p2 IntB = p1      // p2底层类型是*int
	fmt.Println(p1 == p2) // out: true

	p3 := new(IntA)       // p3 类型为 *IntA
	var p4 IntC = p3      // p4 底层类型是 *IntA
	fmt.Println(p3 == p4) // out: true

	//fmt.Println(p1 == p3)  // invalid operation: p1 == p3 (mismatched types *int and *IntA)
}

二、unsafe.Pointer

unsafe.Pointer 是由 unsafe 包提供的类型,它被称为非安全指针类型

// 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 represents a pointer to an arbitrary type.
type Pointer *ArbitraryType

unsafe 包中同时也提供了以下五种函数:

func Sizeof(x ArbitraryType) uintptr

func Offsetof(x ArbitraryType) uintptr

func Alignof(x ArbitraryType) uintptr

// 以下两个函数从 go1.17 开始提供
func Add(ptr Pointer, len IntegerType) Pointer

func Slice(ptr *ArbitraryType, len IntegerType) []ArbitraryType

Sizeof(x ArbitraryType):该函数用来获取一个值的类型的大小(等同于 C 语言的 sizeof)。在同样的编译环境下,同一类型的不同值获取到的类型大小相同。

func main() {
    fmt.Println(unsafe.Sizeof(int(1)))   // 8
	fmt.Println(unsafe.Sizeof(byte(1)))  // 1
	fmt.Println(unsafe.Sizeof(int64(1))) // 8
	fmt.Println(unsafe.Sizeof(int32(1))) // 4

	/*
		在 src/runtime/string.go 中可以发现字符串的结构体实现
		type stringStruct struct {
			str unsafe.Pointer  // 8
			len int             // 8
		}
	*/
	fmt.Println(unsafe.Sizeof("123")) // 16
	fmt.Println(unsafe.Sizeof(""))    // 16

	/*
		在 src/runtime/slice.go 中可以发现字符串的结构体实现
		type slice struct {
			array unsafe.Pointer    // 8
			len   int               // 8
			cap   int               // 8
		}
	*/
	fmt.Println(unsafe.Sizeof([]byte{}))        // 24
	fmt.Println(unsafe.Sizeof([]byte{1, 2, 3})) // 24
    
	fmt.Println(unsafe.Sizeof([2]byte{1, 2}))   // 2
	fmt.Println(unsafe.Sizeof([3]byte{1, 2}))   // 3

	fmt.Println(unsafe.Sizeof(uintptr(1)))                   // 8
	fmt.Println(unsafe.Sizeof(map[string]string{"1": "1"}))  // 8
	fmt.Println(unsafe.Sizeof(map[string]struct{}{"1": {}})) // 8
	fmt.Println(unsafe.Sizeof(make(chan error, 1)))          // 8
}

Alignof(x ArbitraryType):该函数会返回一个类型的对齐值,也可以叫做对齐系数或者对齐倍数。

func main() {
	var (
		b   bool
		i8  int8
		i16 int16
		i64 int64
		f32 float32
		s   string
		p   *int
	)

	fmt.Println(unsafe.Alignof(b))   // 1
	fmt.Println(unsafe.Alignof(i8))  // 1
	fmt.Println(unsafe.Alignof(i16)) // 2
	fmt.Println(unsafe.Alignof(i64)) // 8
	fmt.Println(unsafe.Alignof(f32)) // 4
	fmt.Println(unsafe.Alignof(s))   // 8
	fmt.Println(unsafe.Alignof(p))   // 8
}

Offsetof(x ArbitraryType):该函数用来获取一个结构体某个字段相对于次结构体的地址偏移量。

type User struct {
	Name string
	Age  int
	Sex  byte
}

func main() {
	u := User{
		Name: "Lynx",
		Age:  18,
		Sex:  1,
	}

	fmt.Println(unsafe.Offsetof(u.Name)) // 0
	fmt.Println(unsafe.Offsetof(u.Age))  // 16
	fmt.Println(unsafe.Offsetof(u.Sex))  // 24
	fmt.Println(unsafe.Sizeof(u))        // 32
}

Add(ptr Pointer, len IntegerType):此函数在一个非安全指针表示的地址上添加一个偏移量,然后返回表示新地址的一个指针。

Slice(ptr *ArbitraryType, len IntegerType):此函数从一个任意(安全)指针派生出一个指定长度的切片。

三、uintptr

uintptr 是一个整数类型,能够存放任意指针。uintptr 可以像 C 语言里面的指针一样进行运算操作。但是 Go 里面的类型安全指针不能直接转换成 uintptr 指针,必须先转换成 unsafe.Pointer 类型,再转换成 uintptr

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

四、正确使用 unsafe.Pointer

unsafe 包中列举了六种 unsafe.Pointer 的场景,如果不是以下使用场景,我们在使用 unsafe.Pointer 时可能会无效。

// 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.

1. 类型转换

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

在转换过程中,unsafe.Pointer充当桥梁,将 float64 类型转换为 uint64。 注意目标类型的尺寸不应该大于原始类型,否则会出现溢出异常。

2. Pointer 转换为 uintptr(不逆转换)

unsafe.Pointer 转换为 uintptr 类型,这种转换一般只用于打映内存地址。由于 GC 的存在,将 uintptr 转换回 unsafe.Pointer 的操作可能是无效操作(对象被 GC 回收)。

获得到的 uintptr 只是一个没有指针语义得整数值。当对象地址发生变更时,获得到的 uintptr 不会发生变化。

func main() {
   a := 10
   fmt.Printf("%p\n", &a)
   fmt.Printf("%d\n", uintptr(unsafe.Pointer(&a)))
}

3. 指针运算

我们可以对 uintptr 做运算操作,从而获取到其他地址的值。这种模式常用来访问结构体字段或者数组的地址。

type MyType struct {
	f1 uint8
	f2 int
	f3 uint64
}

func main() {
	fmt.Println("结构体: ")
	s := MyType{f1: 10, f2: 20, f3: 30}
	fmt.Println("f1 地址:", uintptr(unsafe.Pointer(&s)))
	fmt.Println("f1 值:", *(*uint8)(unsafe.Pointer(&s)))
	fmt.Println("f2 地址:", uintptr(unsafe.Pointer(&s))+unsafe.Offsetof(s.f2))
	fmt.Println("f2 值:", *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + unsafe.Offsetof(s.f2))))
	fmt.Println("f3 地址:", uintptr(unsafe.Pointer(&s))+unsafe.Offsetof(s.f3))
	fmt.Println("f3 值:", *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + unsafe.Offsetof(s.f3))))

	// 上面这种进对 uintptr 进行运算的操作,在 go1.17后的版本推介使用 unsafe.Add()
	fmt.Println("f3 地址:", uintptr(unsafe.Add(unsafe.Pointer(&s), unsafe.Offsetof(s.f3))))

	fmt.Println("\n数组: ")
	arr := [3]int{11, 22, 33}
	fmt.Printf("%p\n", &arr)
	for i := 0; i < 3; i++ {
		fmt.Printf("arr[%d] addr: %x\n", i, uintptr(unsafe.Pointer(&arr[0]))+uintptr(i)*unsafe.Sizeof(arr[0]))
		fmt.Printf("arr[%d] value: %d\n", i, *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&arr[0])) + uintptr(i)*unsafe.Sizeof(arr[0]))))
	}
}

❗️❗️❗️注意

在进行上面的指针计算时,不能将获得的地址赋值给临时变量。如下面这种用法就是有问题的:

f2Addr := uintptr(unsafe.Pointer(&s))+unsafe.Offsetof(s.f2)
... other code
fmt.Println(*(*int)(unsafe.Pointer(f2Addr)))

在执行 other code 的过程中,变量 s 可能会因为不被其他任何变量引用,导致被 GC 回收了内存空间。因此在将 f2Addr 转换为 unsafe.Pointer 的时候就会爆出非法指针的错误。

因此,我们在涉及到指针的转换时,需要在一行代码内完成指针转换,因为编译器会保证当前行代码的转换是有效的。

同样的,我们也不能够将获得的 uintptr 作为函数变量传递给其他函数(除了下面这种模式)。

4. 系统调用

在上面的介绍中,我们知道将 uintptr 作为变量传递给一个普通函数是一种非法的行为。

但是,将 uintptr 变量传递给 syscall.Syscall 这样的函数是安全的,其他自定义的函数无法享受到这样的特权。这是编译器进行的安全保证

5. 将 reflect.Value.Pointer/UnsafeAddr 方法的 uintptr 返回值转换为非类型 安全指针

reflect标准库中Value类型的PointerUnsafeAddr方法都返回一个uintptr值,而不是一个unsafe.Pointer值。

Go 团队这样设计的初衷是避免用户不引用unsafe标准库包就可以将这两个方法的返回值转换为任何类型安全指针类型。

但是由于返回的值是 uintptr 类型的,因此必须在返回的时候就立即将其转换为非类型安全的指针,否则就有可能因为 GC 回收变量导致触发非法指针错误。

// 安全
p := (*int)(unsafe.Pointer(reflect.ValueOf(new(int)).Pointer()))

// 不安全!!!
u := reflect.ValueOf(new(int)).Pointer()
// 在这个时刻,处于存储在 u 中的地址处的内存块可能会被回收掉。
p := (*int)(unsafe.Pointer(u))

不过在 go 1.18 版本后,reflect.Value 引入了一个新的方法: func (v Value) UnsafePointer() unsafe.Pointer,该方法返回一个 unsafe.Pointer 变量。官方推介使用这个方法来代替之前的 PointerUnsafeAddr 方法,用以解决原来设计思路的问题。

6. 将 reflect.SliceHeader 或 reflect.StringHeader 的 Data 字段转换成非安全类型,或反之操作

SliceHeaderStringHeader 分别是数字和字符串的实现结构体,由于两个结构体十分类似,可以利用他们实现字符串和 byte 数组零内存拷贝转换。

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

    var b []byte
    pbytes := (*reflect.SliceHeader)(unsafe.Pointer(&b))
    pbytes.Data = stringHeader.Data
    pbytes.Len = stringHeader.Len
    pbytes.Cap = stringHeader.Len

    return b
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值