go语言在设计上确保了一些安全的属性,限制了程序可能出错的途径。例如严格的类型转换规则。但也使得很多实现的细节无法通过go程序来访问,例如对于聚合类型(如结构体)的内存布局,或者一个函数对应的机器码。
这里我们将讨论unsafe包,它是由编译器实现的,实现了对语言内置特性的访问功能,这些特性一般是不可见的,因为它们暴露了go详细的内存布局。虽然包的名字叫unsafe,但是这些函数本身是安全的,并且在做内存优化的时候,它们对理解函数底层内存布局很有帮助。
unsafe.Sizeof
unsafe.Sizeof 报告传递给它的参数在内存中所占的字节长度,这个参数可以是任意类型的表达式。Sizeof仅会报告每个数据结构固定部分的内存所占字节长度,例如指针或者字符串所占的长度,但不会报告例如字符串内容的间接长度。为了可移植性,以字来表示引用类型的长度或者包含引用类型的长度,在32位系统上字的长度是4个字节,而在64位系统上字的长度是8个字节。
package main
import (
"fmt"
"unsafe"
)
func main() {
var x struct{
a bool
b int16
c []int
}
fmt.Println(unsafe.Sizeof(x.a))
fmt.Println(unsafe.Sizeof(x.b))
fmt.Println(unsafe.Sizeof(x.c))
fmt.Println(unsafe.Sizeof(x))
}
// 64位机器结果:
// 1 : 1个字节,bool
// 2 : 两个字节,16/8 = 2
// 24 : 切片24个字节,3个字,因为切片包含一个指针,一个长度,一个容量。
// 32 :前面两个加上内存空位后就是一个字,8个字节。所以 8 + 24 = 32
如果b和c交换位置,那么内存空位将会更大,8 + 24 + 8 = 40
func main() {
var x struct{
a bool
c []int
b int16
}
fmt.Println(unsafe.Sizeof(x.a))
fmt.Println(unsafe.Sizeof(x.c))
fmt.Println(unsafe.Sizeof(x.b))
fmt.Println(unsafe.Sizeof(x))
}
// 64位机器结果:
// 1
// 24
// 2
// 40 这里8 + 24 = 32和我们的猜想一样。
unsafe.Alignof
unsafe.Alignof报告参数类型所要求的对齐方式。这个参数可以是任意类型的表达式,并返回一个常量。布尔类型和数值类型对齐到它们的长度(最大8个字节),其他类型按字对齐。
package main
import (
"fmt"
"unsafe"
)
func main() {
var x struct{
a bool
b int16
c []int
}
fmt.Println(unsafe.Alignof(x.a))
fmt.Println(unsafe.Alignof(x.b))
fmt.Println(unsafe.Alignof(x.c))
fmt.Println(unsafe.Alignof(x))
}
// 64位机器结果:
// 1 :这是布尔类型,布尔类型和数值类型对齐到它们长度
// 2 :这里数值类型,同上
// 8 :其他类型按字对齐,在64位机器上,一个字是8个字节
// 8
unsafe.Offsetof(f)
计算成员f相对于结构体的起始地址的偏移量。如果有内存空位也计算在内,该函数的操作数,必须是一个成员选择器:x.a。
func main() {
var x struct{
a bool
c []int
b int16
}
fmt.Println(unsafe.Offsetof(x.a))
fmt.Println(unsafe.Offsetof(x.c))
fmt.Println(unsafe.Offsetof(x.b))
}
// 64位机器结果:
// 0 : 结构体的第一个成员
// 8 :这里有7个字节的内存空位
// 32
unsafe.Pointer
unsafe.Pointer是一种特殊类型的指针,它可以存储任何变量的地址。对于一个unsafe.Pointer类型的指针,由于我们不知道它的具体类型,导致我们不能间接的通过*p来获取它的实际值。普通类型的指针也可以转换为unsafe.Pointer类型的指针,unsafe.Pointer类型的指针可以转换为普通类型的指针,而且不必和原来的类型相同。使用unsafe.Pointer进行类型转换可以将任意的值写入内存,并因此破坏类型系统。
uintper类型
uintper类型保存了指针所指向地址的数值,这样就可以进行数值运算。(uintpter类型是一个足够大的无符号整型,可以用来表示任何地址。)unsafe.Pointer也可以转换为uintptr,当然uintptr也可以转换为unsafe.Pointer(这里也会破坏类型系统)。
package main
import (
"fmt"
"unsafe"
)
func main() {
var x struct{
a bool
b int16
c []int
}
//pb := &x.b
pb := (*int16)(unsafe.Pointer(uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)))
*pb = 42
fmt.Println(x.b)
}
这里首先将x的地址转换为unsafe.Pointer类型从而转换为uintptr类型,uintptr类型就可以用于计算。计算后再转换为原来的类型,对内存地址指向的区域赋值。但是这里不能引入uintptr类型的临时变量,例如下面这样。因为垃圾回收器会移动内存中的变量,为了减少内存碎片。但是在垃圾回收器uintptr类型仅仅是一个数值,所以移动过后,不会改变uintptr存的指针对应的内存里的数据。
ptr := uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)
pb := (*int16)(unsafe.Pointer(ptr))
总结
unsafe包可以用来操作内存,从而增加go语言的灵活性,但是unsafe包无法保证在未来go语言升级中能够兼容。uintptr类型不能作为临时变量。并且在uintptr类型转换到unsafe.Pointer的过程中要尽量减少uintptr的操作次数。