go绝知—unsafe包
go中指针
先看下go中的指针,go中的指针与c语言中的指针有很大不同:
- go指针不支持运算
- 不同类型无法转换
- 不同类型指针不可比较
我们不难看出,go中的指针更加安全,但是却失去了灵活性,而且有些通过指针高效的处理数据的能力也失去了。
unsafe包
unsafe包的出现,可以让我们更高效的处理数据,但是和他的名字一样,不安全!
我们可以利用unsafe包拿到结构体struct中的未导出字段。
使用unsafe
阅读unsafe包文档中列出的规则:
任何类型的指针值都可以转换为unsafe.Pointer。 unsafe.Pointer可以转换为任何类型的指针值。 uintptr可以转换为unsafe.Pointer。 unsafe.Pointer可以转换为uintptr。
简单而言就是:unsafe.Pointer可以与任何类型指针值转换,unintptr可以与unsafe.Pointer互转。
unsafe 包还有其他三个函数:
func Sizeof(x ArbitraryType) uintptr func Offsetof(x ArbitraryType) uintptr func Alignof(x ArbitraryType) uintptr
ArbitraryType是任意的意思。
获取slice长度
我们换种方式来取slice的长度
type slice struct {
array unsafe.Pointer // 元素指针
len int // 长度
cap int // 容量
}
func main() {
s := make([]int, 9, 20)
var Len = *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + uintptr(8)))
fmt.Println(Len, len(s)) // 9 9
var Cap = *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + uintptr(16)))
fmt.Println(Cap, cap(s)) // 20 20
}
Offsetof 获取成员偏移量
对于一个结构体,通过 offset 函数可以获取结构体成员的偏移量,进而获取成员的地址,读写该地址的内存,就可以达到改变成员值的目的。
这里有一个内存分配相关的事实:结构体会被分配一块连续的内存,结构体的地址也代表了第一个成员的地址。
type Programmer struct {
name string
language string
}
func main() {
p := Programmer{"stefno", "go"}
fmt.Println(p)
name := (*string)(unsafe.Pointer(&p))
*name = "qcrao"
lang := (*string)(unsafe.Pointer(uintptr(unsafe.Pointer(&p)) + unsafe.Offsetof(p.language)))
*lang = "Golang"
fmt.Println(p)
}
这里name成员的地址就是结构体的地址。
string 和 slice 的相互转换
string和slice的转换直接强转即可。
func main() {
str := "abc"
var s []byte = []byte(str)
fmt.Println(cap(s))
str2 := string(s)
fmt.Println(str2)
}
这里转换的时候是有内存重新分配转换的。他们底层虽然都是数组,但是string的底层数组是只读的,不能修改的,slice底层数组是可以修改的。
这是一个非常精典的例子。实现字符串和 bytes 切片之间的转换,要求是 zero-copy。想一下,一般的做法,都需要遍历字符串或 bytes 切片,再挨个赋值。
完成这个任务,我们需要了解 slice 和 string 的底层数据结构:
type StringHeader struct {
Data uintptr
Len int
}
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
我们可以看到,string本质还是个结构体,只需要共享底层 []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))
}
总结
unsafe 包绕过了 Go 的类型系统,达到直接操作内存的目的,使用它有一定的风险性。但是在某些场景下,使用 unsafe 包提供的函数会提升代码的效率,Go 源码中也是大量使用 unsafe 包。