Go unsafe 包
只要阅读Go的源码,就不会错过代码里众多的unsafe.Pointer
等,这个不安全的指针是个啥?
unsafe.Pointer
unsafe.Pointer
可以指向任意类型的指针,这有点类似于C里的void*
。它能突破Go的类型系统限制,读写任意内存,不过还是要谨慎使用。
四个规则限制
- 一个任意类型的指针都能转化成
unsafe.Pointer
unsafe.Pointer
也能转化成一个任意类型的指针uintptr
能转成unsafe.Pointer
unsafe.Pointer
也能转成uintptr
Pointer类型
unsafe包下
type ArbitraryType int
type Pointer *ArbitraryType
Pointer是能指向任意类型的指针,这里ArbitraryType别名了int,实际是为了文档说明,并非只是*int
,可以理解成void*
uintptr
定义
builtin 内建类型
// uintptr is an integer type that is large enough to hold the bit pattern of any pointer.
type uintptr uintptr
uintptr就是个整数类型,足够大,能描述指针地址。比如做指针运算的时候就要把unsafe.Pointer
转成uintptr
,这个后边说。
利用unsafe.Pointer指针遍历slice,并修改其中的元素
package main
import (
"fmt"
"unsafe"
)
func main() {
var sli = []int{1, 2, 3, 4, 5}
for i := 0; i < len(sli); i++ {
p := (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&sli[0])) + uintptr(i)*unsafe.Sizeof(sli[0])))
fmt.Println(*p)
*p = *p + 1
}
fmt.Println(sli) // [2 3 4 5 6]
}
讲解:
(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&sli[0])) + uintptr(i)*unsafe.Sizeof(sli[0])))
这一长串代码,如果不知道上述的四个规则,那根本不会知道它究竟在干嘛。如果熟悉了这几个规则,就可以很容易写出这看似复杂的代码。
- 四个规则的示例 代码拆解
unsafe.Pointer(&sli[0])
- 这里表示获取slice第一个元素的地址-int*类型,并转成unsafe.Pointer。体现了规则的第一条。
uintptr(unsafe.Pointer(&sli[0]))
- 这里需要把第一步的Pointer类型转成uintptr,因为需要做偏移量运算,必须要使用uintptr类型。体现了规则的第三条。
unsafe.Sizeof(sli[0])
- 这里unsafe.Sizeof()函数接收任意一个变量来获取该变量占用内存空间,返回uintptr类型。注意这个空间不包括变量内部指针指向的区域。
uintptr(i)*unsafe.Sizeof(sli[0])
- 因为需要运算slice内的偏移量,所以要把
int i
强转成uintptr类型。
(unsafe.Pointer(uintptr(unsafe.Pointer(&sli[0])) + uintptr(i)*unsafe.Sizeof(sli[0])))
- uintptr运算好之后,因为只有unsafe.Pointer能和变量指针互相转化,所以要转回unsafe.Pointer。体现了规则的第三条。
(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&sli[0])) + uintptr(i)*unsafe.Sizeof(sli[0])))
- 最后把 unsafe.Pointer 转回 *int,又回到了原始int指针。提现了规则的第二条。
利用unsafe.Pointer指针修改结构体属性
以下是64位机器
type student struct {
b bool
name string
age int
}
xiaoming := student{true, "xiaoming", 18}
fmt.Println(unsafe.Sizeof(xiaoming.b)) // 1
fmt.Println(unsafe.Sizeof(xiaoming.name)) // 16
fmt.Println(unsafe.Sizeof(xiaoming.age)) // 8
// 注意这里
fmt.Println(unsafe.Sizeof(xiaoming)) // 32
fmt.Println(unsafe.Offsetof(xiaoming.b)) // 0
fmt.Println(unsafe.Offsetof(xiaoming.name)) // 8
fmt.Println(unsafe.Offsetof(xiaoming.age)) // 24
fmt.Println(xiaoming) // {true xiaoming 18}
*(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&xiaoming)) + unsafe.Offsetof(xiaoming.age))) = 100
*(*string)(unsafe.Pointer(uintptr(unsafe.Pointer(&xiaoming)) + unsafe.Offsetof(xiaoming.name))) = "xiaobai"
fmt.Println(xiaoming) // {true xiaobai 100}
内存对齐
看例子会发现一个问题,xiaoming各个属性的size 分别是1,16,8。但是xiaoming的size是32,而非1+16+8。并且xiaoming.name的offset是8,而不是1。
原因就是内存对齐,内存分配过程中,为了更快的运算和获取,需要内存对齐。我当前64位机器,最大的对齐是8个字节。b bool 占一个字节,后边就要填充7个字节的随机值来补充到8个字节达到内存对齐的效果。
能说明的是 如果编译器不做字段顺序的优化,那么结构体内不同的字段顺序可能会有不同的大小占用。
Sizeof Offsetof
因为内存对齐的原因,在获取结构体字段内存偏移量时候,不能贸然使用前面几个字段的Sizeof
相加,要使用 Offsetof
。当然如果编译器对结构体顺序做优化也是一个原因。
另外 Sizeof
的用法也可以使用反射reflect.TypeOf(var).Size()
表示
常见类型的Sizeof
以下针对64位机器,任意类型指针int*
是8个字节,表示一个字长。
类型 | 字长 | 备注 |
---|---|---|
bool | - | 1个字节 |
int, uint, uintptr | 1 | - |
intN, uintN, floatN, complexN | N/8 | - |
string | 2 | 结构组成:data指针和int长度 |
[]T | 3 | 结构组成:data指针、int长度、int容量 |
*T | 1 | 任意类型指针 |
map | 1 | 引用 |
chan | 1 | 引用 |
func | 1 | 引用 |
interface | 2 | 结构组成:value指针和type指针 |
总结:
Go的unsafe包很重要,这里对包的Pointer类型和几个函数Sizeof
、Offsetof
做了介绍。最关键的还是要记住那四个规则。
了解Go的常见类型的size。如map
、chan
、func
这些都是引用,slice
和string
的结构组成,interface
比较重要是2个字长 一个类型指针一个值指针。