1. Go指针相对于C指针的区别
Go 的指针多了一些限制。使其既可以享受指针带来的便利,又避免了指针的危险性。
限制一:Go 的指针不能进行数学运算。
a := 5
p := &a
p++
p = &a + 3
上面的代码将不能通过编译:invalid operation,即不能对指针做数学运算。
限制二:不同类型的指针不能相互转换。
func main() {
a := int(100)
var f *float64
f = &a
}
也会报编译错误:cannot use &a (type *int) as type *float64 in assignment
限制三:不同类型的指针不能使用 == 或 != 比较。
只有在两个指针类型相同或者可以相互转换的情况下,才可以对两者进行比较。另外,指针可以通过 == 和 != 直接和 nil 作比较。
限制四:不同类型的指针变量不能相互赋值。
这一点同限制三。
2. unsafe前导
2.1 什么是 unsafe
前面所说的指针是类型安全的,但它有很多限制。Go 还有非类型安全的指针,这就是 unsafe 包提供的 unsafe.Pointer。在某些情况下,它会使代码更高效,当然,也更危险。
unsafe 包用于 Go 编译器,在编译阶段使用。 它可以绕过 Go 语言的类型系统,直接操作内存。例如,一般我们不能操作一个结构体的未导出成员,但是通过 unsafe 包就能做到。unsafe 包让我可以直接读写内存,还管你什么导出还是未导出。
2.2 为什么有 unsafe
Go 语言类型系统是为了安全和效率设计的,有时,安全会导致效率低下。有了 unsafe 包,高阶的程序员就可以利用它绕过类型系统的低效。因此,它就有了存在的意义。
2.3 unsafe 实现原理
type ArbitraryType int
type Pointer *ArbitraryType
从命名来看,Arbitrary 是任意的意思,也就是说 Pointer 可以指向任意类型。
unsafe 包还有其他三个函数:
func Sizeof(x ArbitraryType) uintptr
func Offsetof(x ArbitraryType) uintptr
func Alignof(x ArbitraryType) uintptr
Sizeof 返回类型 x 所占据的字节数,但不包含 x 所指向的内容的大小。例如,对于一个指针,函数返回的大小为 8 字节(64位机上)。
Offsetof 返回结构体成员在内存中的位置离结构体起始处的字节数,所传参数必须是结构体的成员。
Alignof 返回 m,m 是指当类型进行内存对齐时,它分配到的内存地址能整除 m。
注意到以上三个函数返回的结果都是 uintptr 类型,这和 unsafe.Pointer 可以相互转换。三个函数都是在编译期间执行,它们的结果可以直接赋给 const 型变量。另外,因为三个函数执行的结果和操作系统、编译器相关,所以是不可移植的。
综上所述,unsafe 包提供了 2 点重要的能力:
- 任何类型的指针和 unsafe.Pointer 可以相互转换。
- uintptr 类型和 unsafe.Pointer 可以相互转换。
pointer 不能直接进行数学运算,但可以把它转换成 uintptr,对 uintptr 类型进行数学运算,再转换成 pointer 类型。
还有一点要注意的是,uintptr 并没有指针的语义,即 uintptr 所指向的对象会被 gc 无情地回收。而 unsafe.Pointer 有指针语义,可以保护它所指向的对象在“有用”的时候不会被垃圾回收。
3. unsafe 如何使用
3.1 获取 slice 长度
我们可以通过 unsafe.Pointer 和 uintptr 进行转换,得到 slice 的字段值。
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
}
Len,cap 的转换流程如下:
Len: &s => pointer => uintptr => pointer => *int => int
Cap: &s => pointer => uintptr => pointer => *int => int
3.2 获取 map 长度
我们依然能通过 unsafe.Pointer 和 uintptr 进行转换,得到 hmap 字段的值,只不过,现在 count 变成二级指针了:
func main() {
mp := make(map[string]int)
mp["qcrao"] = 100
mp["stefno"] = 18
// mp 是指针, &mp是指针的指针(二级指针),转换成 unsafePointer后需强转成二级指针
count := **(**int)(unsafe.Pointer(&mp))
fmt.Println(count, len(mp)) // 2 2
}
count 的转换过程:
&mp => pointer => **int => int
3.3 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)
}
运行代码,输出:
{stefno go}
{qcrao Golang}
name 是结构体的第一个成员,因此可以直接将 &p 解析成 *string。这一点,在前面获取 map 的 count 成员时,用的是同样的原理。
对于结构体的私有成员,现在有办法可以通过 unsafe.Pointer 改变它的值了。
3.5 string 和 slice 的相互转换
一个非常经典的例子。实现字符串和 bytes 切片之间的转换,要求是 zero-copy。一般的做法需要遍历字符串或 bytes 切片,再挨个赋值。
完成这个任务,我们需要了解 slice 和 string 的底层数据结构:
type StringHeader struct {
Data uintptr
Len int
}
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
上面是反射包下的结构体,路径:src/reflect/value.go。只需要共享底层 []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))
}
func string2bytes(s string) []byte { // 简化版本,编译器自动帮我们做了上面的操作
return *(*[]byte)(unsafe.Pointer(&s))
}
代码比较简单,不作详细解释。通过构造 slice header 和 string header,来完成 string 和 byte slice 之间的转换。
总结
unsafe 包绕过了 Go 的类型系统,达到直接操作内存的目的,使用它有一定的风险性。但是在某些场景下,使用 unsafe 包提供的函数会提升代码的效率,Go 源码中也是大量使用 unsafe 包。
unsafe 包定义了 Pointer 和三个函数:
type ArbitraryType int
type Pointer *ArbitraryType
func Sizeof(x ArbitraryType) uintptr
func Offsetof(x ArbitraryType) uintptr
func Alignof(x ArbitraryType) uintptr
通过三个函数可以获取变量的大小、偏移、对齐等信息。
uintptr 可以和 unsafe.Pointer 进行相互转换,uintptr 可以进行数学运算。这样,通过 uintptr 和 unsafe.Pointer 的结合就解决了 Go 指针不能进行数学运算的限制。
通过 unsafe 相关函数,可以获取结构体私有成员的地址,进而对其做进一步的读写操作,突破 Go 的类型安全限制。
本文为转载内容,转自 饶全成老师的《# 深度解密 Go 语言之 unsafe》,进行了部分修改
原文链接:<qcrao.com/post/dive-i… >