golang中unsafe的Q&A


标准库 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)

相关