Go指针和unsafe.Pointer有什么区别
- Go 的指针不能进行数学运算
- 不同类型的指针不能相互转换
- 不同类型的指针不能使用 == 或 != 比较
- 不同类型的指针变量不能相互赋值
unsafe 包提供了 2 点重要的能力:
pointer 不能直接进行数学运算,但可以把它转换成 uintptr,对 uintptr 类型进行数学运算,再转换成 pointer 类型。
// uintptr 是一个整数类型,它足够大,可以存储
type uintptr uintptr
复制代码
还有一点要注意的是,uintptr 并没有指针的语义,意思就是 uintptr 所指向的对象会被 gc 无情地回收。而 unsafe.Pointer 有指针语义,可以保护它所指向的对象在“有用”的时候不会被垃圾回收。
unsafe 包中的几个函数都是在编译期间执行完毕,毕竟,编译器对内存分配这些操作“了然于胸”。在 unsafe.go 路径下,可以看到编译期间 Go 对 unsafe 包中函数的处理。
如何利用unsafe包修改私有成员
对于一个结构体,通过 offset
函数可以获取结构体成员的偏移量,进而获取成员的地址,读写该地址的内存,就可以达到改变成员值的目的。
这里有一个内存分配相关的事实:结构体会被分配一块连续的内存,结构体的地址也代表了第一个成员的地址。
我们来看一个例子:
package main
import (
"fmt"
"unsafe"
)
type Programmer struct {
name string
language string
}
func main() {
p := Programmer{"stefno", "go"}
fmt.Println(p)
name := (*string)(unsafe.Pointer(&p))
*name = "qcrao"
//unsafe.Offsetof 返回结构开头和字段开头之间的字节数
lang := (*string)(unsafe.Pointer(uintptr(unsafe.Pointer(&p)) + unsafe.Offsetof(p.language)))
*lang = "Golang"
fmt.Println(p)
}
复制代码
运行代码,输出:
{stefno go}
{qcrao Golang}
复制代码
name 是结构体的第一个成员,因此可以直接将 &p 解析成 *string。 对于结构体的私有成员,现在有办法可以通过 unsafe.Pointer 改变它的值了。 我把 Programmer 结构体升级,多加一个字段:
type Programmer struct {
name string
age int
language string
}
复制代码
并且放在其他包,这样在 main 函数中,它的三个字段都是私有成员变量,不能直接修改。但我通过 unsafe.Sizeof() 函数可以获取成员大小,进而计算出成员的地址,直接修改内存。
func main() {
p := Programmer{"stefno", 18, "go"}
fmt.Println(p)
lang := (*string)(unsafe.Pointer(uintptr(unsafe.Pointer(&p)) + unsafe.Sizeof(int(0)) + unsafe.Sizeof(string(""))))
*lang = "Golang"
fmt.Println(p)
}
复制代码
这里仅仅是对如何设置私有变量的方法进行扩展说明,在当前场景下,unsafe.Offsetof(p.language)
能够替代unsafe.Sizeof(int(0)) + unsafe.Sizeof(string(""))
输出:
{stefno 18 go}
{stefno 18 Golang}
复制代码
字符串和byte切片的零拷贝转换
实现字符串和 bytes 切片之间的转换,要求是 zero-copy
。想一下,一般的做法,都需要遍历字符串或 bytes 切片,再挨个赋值。
type StringHeader struct {
Data uintptr
Len int
}
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
复制代码
只需要共享底层 []byte 数组就可以实现 zero-copy
。
func string2bytes(s string) []byte {
stringHeader := (*reflect.StringHeader)(unsafe.Pointer(&s))
bh := reflect.SliceHeader{
Data: stringHeader.Data,
Len: stringHeader.Len,
Cap: stringHeader.Len,
}
return *(*[]byte)(unsafe.Pointer(&bh))
}
func bytes2string(b []byte) string{
sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&b))
sh := reflect.StringHeader{
Data: sliceHeader.Data,
Len: sliceHeader.Len,
}
return *(*string)(unsafe.Pointer(&sh))
}
复制代码
代码比较简单,不作详细解释。通过构造 slice header 和 string header,来完成 string 和 byte slice 之间的转换。