标准库 unsafe 的 Q&A
go指针和 unsafe.Pointer有什么区别?
1. go语言的作者ken thompson也是c语言的作者,所以go可以看做c系语言,它的很多特性和c类似,
指针就是其中之一,然而go语言的指针相比c语言的指针又很多限制,当然也是为了安全考虑,
相比于c语言指针的灵活,go语言的指针多了一些限制,但这也算go的成功之处,即可以享受指针带来的便利,
又避免了指针的危害性
2. 限制1:go的指针不能做数学运算
func main() {
a := 5
p := &a
p++
p = &a + 3
}
上面的例子是无法运行的,会报错:invalid operation, 也就是说不能对指针做数学运算
3. 限制2:不同类型的指针不能相互转换
func main() {
var a = int(100)
var f *float64 // 声明一个指针类型的变量
f = &a
fmt.Println(f)
}
报错:cannot use &a (type *int) as type *float64 in assignment,也就是说不同类型的指针不能相互转换
4. 限制3:不同类型的指针不能使用 == 或 !=
func main() {
var a *int
var f *float64 // 声明一个指针类型的变量
fmt.Println(a == f)
}
报错:invalid operation: a == f (mismatched types *int and *float64)
5. 限制4,不同类型的指针变量不能相互赋值
func main() {
var i = 15
var b *int = &i
var c *float64
c = b // 不同类型的指针变量不能相互赋值
}
unsafe包提供了两个重要的能力
1. 任何类型的指针和unsife.Pointer可以相互转换
2. uintptr类型和unsafe.Pointer可以相互转换
3. pointer不能直接进行数学运算,可以把它转换成uintptr,对uintptr类型进行数学运算,然后再转换成pointer
如何利用unsafe包修改私有成员
1. 对于一个结构体,可以通过offset函数获取结构体成员的偏移量,进而获取成员的地址,读写该地址的内存
就可以达到改变成员的目的
2. 这里有一个内存分配相关事实,结构体会被分配一块连续的内存,结构体的地址也代表了第一个成员的地址
type Programmer struct {
name string
language string
}
func main() {
p := Programmer{"lisi", "go"}
name := (*string)(unsafe.Pointer(&p))
fmt.Println(name, *name) // 0xc0000203e0 lisi
language := (*string)(unsafe.Pointer(uintptr(unsafe.Pointer(&p)) + unsafe.Offsetof(p.language)))
fmt.Println(language, *language) // 0xc0000203f0 go
}
3. name是结构体的第一个成员,所以可以直接将&p解析成*string,
* 对于结构体的私有成员,现在有办法通过unsafe.Pointer改变它的值了
type Programmer struct {
name string
age int
language string
}
var P = Programmer{name: "li", age: 18, language: "go"}
并且放在其它包,这样在main函数中,它的三个字段都是私有成员变量,不能直接修改,
但是我们通过unsafe.Sizeof函数可以获取成员大小,进而计算出成员的地址,直接修改内存
func main() {
p := test.P
fmt.Println(p) // {li 18 go}
// p.language = "java" // 报错:p.language undefined (cannot refer to unexported field or method language)
lang := (*string)(unsafe.Pointer(uintptr(unsafe.Pointer(&p)) + unsafe.Sizeof(0) + unsafe.Sizeof("")))
*lang = "python"
fmt.Println(p) // {li 18 python}
}
4. unsafe.Pointer可以实现修改其它包中的私有成员
获取slice的长度
1. slice源码 src/runtime/slice.go
type slice struct {
array unsafe.Pointer
len int
cap int
}
2. 因此我们可以通过unsafe.Pointer和uintptr进行转换,得到slice的字段值
func main() {
var s = make([]int, 11, 30)
//Len := *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + unsafe.Offsetof("len"))) // 会报错
//Len := *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + unsafe.Sizeof(0))) // 方法1
Len := *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + uintptr(8))) // 方法2
fmt.Println(Len, len(s)) // 11 11
//Cap := *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + unsafe.Sizeof(0) + unsafe.Sizeof(0))) // 方法1
Cap := *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + uintptr(16))) // 方法2
fmt.Println(Cap, cap(s)) // 30 30
}
3. len cap 的转换流程如下
Len: &s => pointer => uintptr => pointer => *int => int
Cap: &s => pointer => uintptr => pointer => *int => int
获取map的长度
type hmap struct {
// Note: the format of the hmap is also encoded in cmd/compile/internal/reflectdata/reflect.go.
// Make sure this stays in sync with the compiler's definition.
count int // # live cells == size of map. Must be first (used by len() builtin)
flags uint8
B uint8 // log_2 of # of buckets (can hold up to loadFactor * 2^B items)
noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details
hash0 uint32 // hash seed
buckets unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.
oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing
nevacuate uintptr // progress counter for evacuation (buckets less than this have been evacuated)
extra *mapextra // optional fields
}
1. 和slice不同的是,makemap返回的是hmap的指针类型,注意是指针
`func makemap(t *maptype, hint int, h *hmap) *hmap {`
2. 我们依然能通过unsafe.Pointer和uintptr进行转换,得到hmap字段的值,只不过,
现在count变成二级指针了
func main() {
m := make(map[string]int)
m["age"] = 18
m["age2"] = 22
Len := **(**int)(unsafe.Pointer(&m))
fmt.Println(Len, len(m)) // 2 2
}
3. count的转换过程
`&mp => pointer => **int => int`
如何实现字符串和byte切片的零拷贝转换
func main() {
str1 := "马亚南"
fmt.Println(str1) // 马亚南
b1 := []byte(str1) // 字符串转换成字符切片
fmt.Println(b1, len(b1)) // [233 169 172 228 186 154 229 141 151] 9
r1 := []rune(str1) // 字符串转换成rune切片
fmt.Println(r1, len(r1)) // [39532 20122 21335] 3
fmt.Println(utf8.RuneCountInString(str1)) // 3
fmt.Println(utf8.RuneCount(b1)) // 3
}
1. 这是一个非常经典的例子,实现字符串和bytes切片之间的转换,要求是zero-copy(零拷贝),
想象一下,一般的做法都需要遍历字符串或bytes切片,在挨个赋值
2. 完成这个任务,我们需要了解slice和string的底层数据结构体
type StringHeader struct {
Data uintptr
Len int
}
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
3. 上面是反射包下的结构体,路径 src/reflect/value.go,只需要共享底层[]byte数组,就可以实现零拷贝
func String2Bytes(s string) []byte {
/* 字符串转bytes---并且是零拷贝(zero-copy) */
// 字符串转换成地城结构体或底层结构体指针
//stringHeader := *(*reflect.StringHeader)(unsafe.Pointer(&s))
stringHeader := (*reflect.StringHeader)(unsafe.Pointer(&s))
bh := reflect.SliceHeader{
Data: stringHeader.Data,
Len: stringHeader.Len,
Cap: stringHeader.Len,
}
// 将结构体指针类型转换成[]byte类型
return *(*[]byte)(unsafe.Pointer(&bh))
}
func Bytes2String(b []byte) string {
/* 字节类型转换成字符串类型---并且是零拷贝(zero-copy) */
sliceHeader := *(*reflect.SliceHeader)(unsafe.Pointer(&b))
stringHeader := reflect.StringHeader{
Data: sliceHeader.Data,
Len: sliceHeader.Len,
}
return *(*string)(unsafe.Pointer(&stringHeader))
}
func main() {
b1 := String2Bytes("哈哈哈")
fmt.Println(b1) // [229 147 136 229 147 136 229 147 136]
s1 := Bytes2String(b1)
fmt.Println(s1) // 哈哈哈
}
* 通过string header和slice header完成string和[]byte之间的零拷贝(zero-copy)