1. Go中对指针的限制
- Go 的指针不能进行数学运算。
- 不同类型的指针不能相互转换。
- 不同类型的指针不能使用 == 或 != 比较。只有在两个指针类型相同或者可以相互转换的情况下,才可以对两者进行比较。另外,指针可以通过 == 和 != 直接和 nil 作比较。
- 不同类型的指针变量不能相互赋值。
使用unsafe包,可以一定程度上打破这些限制,那么为什么要打破这些限制。请看下文。
2. unsafe.Pointer
unsafe.Pointer的定义
type ArbitraryType int
type Pointer *ArbitraryType
unsafe 包提供了 2 点重要的能力:
- 任何类型的指针和 unsafe.Pointer 可以相互转换。
- uintptr 类型和 unsafe.Pointer 可以相互转换。
pointer 不能直接进行数学运算,但可以把它转换成 uintptr,对 uintptr 类型进行数学运算,再转换成 pointer 类型。利用这两个对象的相互转换,就可以打破上述4个限制。
// uintptr 是一个整数类型,它足够大,可以存储
type uintptr uintptr
还有一点要注意的是,uintptr 并没有指针的语义,意思就是 uintptr 所指向的对象会被 gc 无情地回收.而 unsafe.Pointer 有指针语义,可以保护它所指向的对象在“有用”的时候不会被垃圾回收。
3. 利用unsafe获取slice和map的长度
slice和map的长度都存储在其内部变量中,因此我们先来看这两个结构体定义:
// runtime/slice.go
type slice struct {
array unsafe.Pointer // 元素指针
len int // 长度
cap int // 容量
}
调用 make 函数新建一个 slice,底层调用的是 makeslice 函数,返回的是 slice 结构体:
func makeslice(et *_type, len, cap int) slice
因此我们可以通过 unsafe.Pointer 和 uintptr 进行转换,得到 slice 的字段值。
func main() {
s := make([]int, 9, 20)
// Len: &s => pointer => uintptr => pointer => *int => int
var Len = *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + uintptr(8)))
fmt.Println(Len, len(s)) // 9 9
// Cap: &s => pointer => uintptr => pointer => *int => int
var Cap = *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + uintptr(16)))
fmt.Println(Cap, cap(s)) // 20 20
}
总体思路是,先对结构体取指针,再转换成unsafe.Pointer,再转换成uintptr,然后做加法使指针偏移,然后重新转换为unsafe.Pointer,再转换成(*int)类型,最后取引用,得到值。可以看出,其中的重点在于计算偏移量。
对于map
type hmap struct {
count int
flags uint8
B uint8
noverflow uint16
hash0 uint32
buckets unsafe.Pointer
oldbuckets unsafe.Pointer
nevacuate uintptr
extra *mapextra
}
和 slice 不同的是,makemap 函数返回的是 hmap 的指针,注意是指针:
func makemap(t *maptype, hint int64, h *hmap, bucket unsafe.Pointer) *hmap
我们依然能通过 unsafe.Pointer 和 uintptr 进行转换,得到 hamp 字段的值,只不过,现在 count 变成二级指针了:
func main() {
mp := make(map[string]int)
mp["q"] = 100
mp["s"] = 18
// &mp => pointer => **int => int
count := **(**int)(unsafe.Pointer(&mp))
fmt.Println(count, len(mp)) // 2 2
}
4. 修改私有成员
总体思路是:通过 offset 函数获取结构体成员的偏移量,进而获取成员的地址,读写该地址的内存,就可以达到改变成员值的目的。
这里需要注意的是:结构体会被分配一块连续的内存,结构体的地址也代表了第一个成员的地址。
package main
import (
"fmt"
"unsafe"
)
type Programmer struct {
name string
language string
}
func main() {
p := Programmer{"a", "go"}
fmt.Println(p)
name := (*string)(unsafe.Pointer(&p))
*name = "b"
lang := (*string)(unsafe.Pointer(uintptr(unsafe.Pointer(&p)) + unsafe.Offsetof(p.language)))
// lang := (*string)(unsafe.Pointer(uintptr(unsafe.Pointer(&p)) + unsafe.Sizeof(string(""))))
*lang = "Golang"
fmt.Println(p)
}
5. 字符串和byte切片的零拷贝转换
string和byte切片的内部结构如下:
type StringHeader struct {
Data uintptr
Len int
}
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
转换:
func string2bytes(s string) []byte {
return *(*[]byte)(unsafe.Pointer(&s))
}
func bytes2string(b []byte) string{
return *(*string)(unsafe.Pointer(&b))
}
但这样转换会存在一个问题,字符串被转换为切片后,切片的cap无法准确得出。