unsafe.Pointer&uintptr

前言

本文将介绍下golang源码里经常出现的东西: unsafe.Pointeruintptr

工作中很少接触到这两个,但是平时如果看源码的话对这两个就不陌生了。所以还是有必要记录一下。

本文代码基于golang 1.18

unsafe.Pointer

Pointer代表一个可以指向任何类型的指针,注意是任何。

golang源码对它的描述如下:

1
2
3
4
5
6
7
8
// 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.
// Pointer therefore allows a program to defeat the type system and read and write
// arbitrary memory. It should be used with extreme care.

简单总结下,Pointer作为一个指针,可以无视golang强类型语言的特性,在任意的内存区域内读写值。这个特性很强,但大家在项目中最好不要随便使用,不然你猜他为什么放在unsafe(不安全)包下

平常有四种关于Pointer的常用操作:

  1. 任意的指针类型可以转换成Pointer
  2. 一个Pointer可以转换成任意的指针类型
  3. uintptr可以转换成Pointer
  4. Pointer可以转换成uintptr

举个例子

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
func main() {
	// a为int8的指针, b为int16的指针
	a, b := new(int8), new(int16)
	// 分别给a, b赋值
	*a = 10
	*b = 20
	// output: 10, 20
	fmt.Println(*a, *b)

	// cannot use *b (variable of type int16) as type int8 in assignment
	*a = *b
}

这里我们想将a指向b,但是编译器提示错误,原因就是它们两不是同一类型,将程序改下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
func main() {
	// a为int8的指针, b为int16的指针
	a, b := new(int8), new(int16)
	// 分别给a, b赋值
	*a = 10
	*b = 20
	// output: 10, 20
	fmt.Println(*a, *b)

	pb := unsafe.Pointer(b)
	int8a := (*int8)(pb)
	*a = *int8a
	// output: 20, 20
	fmt.Println(*a, *b)
}

例子中的转换流程:

  1. b(type: *int16) => unsafe.Pointer (任意的指针类型可以转换成Pointer)
  2. unsafe.Pointer => *int8 (一个Pointer可以转换成任意的指针类型)
  3. 最后将a指向转换后的*int8地址,实现两种不同类型的转换

Pointer的具体数据类型:

1
2
3
4
5
type Pointer *ArbitraryType

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

可以看到PointerArbitraryType的指针类型,而后者本质上是int类型,这里只是为了语义化,所以取了一个ArbitraryType(任意类型)名字

uintptr

golang中定义如下

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

uintptr本质上还是一个整形,只不过它能装载的范围很大,足以装载任何类型的指针。

这里需要注意它和unsafe.Pointer的区别。前者保存了地址的整形,可以利用它对地址进行运算。因为它不实际持有引用,所以垃圾回收器可能会回收它所代表的内存区域。后者是个指针类型,可以指向任何区域。

举个例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
type User struct {
	Name string
	Age  int
}

func main() {
	u := &User{}
	// 将u的指针类型转换为unsafe.Pointer
	uAddr := unsafe.Pointer(u)
	// 计算出age字段的偏移量
	ageOffset := unsafe.Offsetof(u.Age)
	// 根据age字段的偏移量,得到age字段的具体内存地址
	ageAddr := unsafe.Pointer(uintptr(uAddr) + ageOffset)
	// 给age字段赋值
	*(*int8)(ageAddr) = 20
	// output: 20
	fmt.Println(u.Age)
}

例子中的操作逻辑:

  1. 先申明一个User实例,并获取到类型为unsafe.Pointer的实例指针
  2. 调用unsafe.Offsetof方法获取到age字段距离结构体内存地址的偏移量(该方法传入一个结构体字段即可获得偏移量)
  3. 依据偏移量+user实例的内存地址,得到User.Age字段的内存地址
  4. 对该地址赋值

这个例子展示了 uintptr可以转换成PointerPointer可以转换成uintptr 我们得到uintptr之后就可以进行指针运算了。

后记

本文从golang源码上介绍了unsafe.Pointeruintptr两个结构,在golang源码中广泛用到了它们。算是看源码的前置知识了。

总结一下:

  • unsafe.Pointer底层是一个整形的指针类型,可以读写内存中的任何一块区域,并且可以与任意类型互相转换(编译上不会报错)
  • uintptr底层是一个整形,非指针类型。因为是非指针类型,所以它保存内存地址所对应的内存区域可能会被清除。它可以用于指针运算

特别注意:在项目中最好不要使用,因为你不能保证操作内存无误

updatedupdated2023-05-222023-05-22