unsafe 的用法
Go 在常规操作下是类型安全的。所谓类型安全是指一块内存地址数据一旦被特定的类型所解析,它就不能解析为其他类型,不能再与其他类型建立关联。
Go 语言的类型安全建立在 Go 编译器的静态检查以及 Go 运行时利用类型信息进行的运行时检查之上的。在语法层面做了诸多限制:
- 不支持隐式类型转换,所有类型必须显式进行
- 不支持指针运算
在考虑类型安全的同时,为了兼顾性能以及如何实现与操作系统、C 代码等互操作的低级代码等问题。最终 Go 语言选择在类型系统上开一道后门,即标准库内置一个特殊的 unsafe 包。
使用 unsafe 包我们可以实现性能更高,与底层系统交互更容易的低级代码,但 unsafe 包的存在也让我们有了绕过 Go 类型安全的屏障的路径。 一旦使用不当,便可能会导致引入安全漏洞、引发程序崩溃等问题。
unsafe 包之所以被称为 unsafe,主要是因为该包定义了 unsafe.Pointer 类型。unsafe.Pointer 可用于表示任意类型的指针,并且它具备下面四条其他指针类型所不具备的特性:
- 任意类型的指针值可以被转换为 unsafe.Pointer
- unsafe.Pointer 可以被转换为任意类型的指针值
- uintptr 可以被转换为 unsafe.Pointer
- unsafe.Pointer 可以被转换为 uintptr
有了 unsafe.Pointer 的四个特性,我们发现通过 unsafe.Pointer,可以很容易穿透 Go 的类型安全保护:
func main() {
var a uint32 = 0x12345678
fmt.Printf("%x\n", a) // 12345678
p := (unsafe.Pointer)(&a)
b := (*[4]byte)(p)
b[0] = 0x23
b[1] = 0x45
b[2] = 0x67
b[3] = 0x8a
fmt.Printf("%x\n", a) // 8a674523
}
unsafe 包的API
Go 标准库中的 unsafe 包非常简洁,下面几行代码就是 unsafe 包的全部内容:
func Alignof(x ArbitraryType) uintptr
func Offsetof(x ArbitraryType) uintptr
func Sizeof(x ArbitraryType) uintptr
type ArbitraryType int
type Pointer *ArbitraryType
虽然位于 unsafe 包,但 Alignof
、Offsetof
、Sizeof
这三个函数的使用是绝对安全的。
这三个函数两个共同点:1、接收的参数是一个表达式,而不是一个类型,ArbitraryType
表示任意表达式的类型;2、返回值都是 uintptr
类型。
采用 uintptr
作为返回值类型可以减少指针运算表达式中的显式类型转换。
Sizeof
Sizeof 函数返回操作数在内存中的字节大小。操作数可以是任意类型的表达式,但是它并不会对操作数进行求值。
type Foo struct {
a int
b string
c [10]byte
d float64
}
var i int = 5
var a = [100]int{}
var s1 = a[:]
var f Foo
println(unsafe.Sizeof(i)) // 8
println(unsafe.Sizeof(a)) // 800
println(unsafe.Sizeof(s1)) // 24 返回的是切片描述符的大小
println(unsafe.Sizeof(f)) // 48
println(unsafe.Sizeof(f.c)) // 10
println(unsafe.Sizeof((*int)(nil))) // 8
Sizeof 函数不支持直接传入无类型信息的 nil 值,必须显式告知 Sizeof 函数 nil 的类型信息,就像上面的代码那样。
Alignof
Alignof 函数用于获取一个表达式的内存地址对齐系数。对于一个类型为 T 的表达式 x,Alignof(x) 的结果是 T 类型的值在内存中的对齐系数。
对齐系数是一个计算机体系架构层面的术语。不同的计算机体系结构的处理器对变量地址都有着对齐要求,即变量的地址必须可被该变量的对齐系数整除。
var x unsafe.ArbitraryType
b := uintptr(unsafe.Pointer(&x)) % unsafe.Alignof(x) == 0
println(b) // true
我们来看一下 unsafe.Alignof
的使用例子:
type Foo struct {
a int
b string
c [10]byte
d float64
}
var i int = 5
var a = [100]int{}
var s1 = a[:]
var f Foo
println(unsafe.Alignof(i)) // 8
println(unsafe.Alignof(a)) // 8
println(unsafe.Alignof(s1)) // 8
println(unsafe.Alignof(f)) // 8
println(unsafe.Alignof(f.a)) // 8
println(unsafe.Alignof(f.c)) // 1
println(unsafe.Alignof(structP{}{})) // 1
println(unsafe.Alignof([0]int{})) // 8
Offsetof
Offsetof 函数用于获取一个结构体成员在结构体中的偏移量。对于一个结构体类型为 T 的表达式 x 和一个字段 f,Offsetof(T.f) 的结果是 f 字段相对于结构体 T 起始地址的偏移量。
Offsetof 应用面较窄,仅用于求结构体中某字段的偏移值。
println(unsafe.Offsetof(f.a)) // 0
println(unsafe.Offsetof(f.b)) // 8
println(unsafe.Offsetof(f.c)) // 24
println(unsafe.Offsetof(f.d)) // 40
unsafe 包典型应用
unsafe 被广泛应用于 Go 标准库和 Go 运行时的实现当中,reflect 包、sync 包、syscall 包、runtime 包都是 unsafe 包的重度用户。 在开源项目中,Gopher 对 unsafe 包也是青睐有加。Go bingding 项目、网络领域项目和数据库领域项目是 unsafe 的重度用户。 其中 unsafe.Pointer 则是 unsafe 包中的被使用最多的特性,占据9成以上份额。
unsafe 包这些项目中主要被用于如下两个场景:
1、与操作系统以及非 Go 编写的代码的通信
与操作系统的通信主要通过系统调用进行,而与非 Go 编写的代码的通信主要通过 cgo 方式:
func SetIcon(iconBytes []byte) {
cstr := (*C.char)(unsafe.Pointer(&iconBytes[0]))
C.SetIcon(cstr, C.int(len(iconBytes)))
}
2、高效类型转换
使用 unsafe 包可以实现高效的类型转换,最常见的类型转换是 string 与 []byte 类型间的相互转换:
func BytesToString(b []byte) string {
return *(*string)(unsafe.Pointer(&b))
}
func StringToBytes(s string) []byte {
sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
bh := reflect.SliceHeader{
Data: sh.Data,
Len: sh.Len,
Cap: sh.Len,
}
return *(*[]byte)(unsafe.Pointer(&bh))
}
通过上面的基于 unsafe 包实现的 StringToBytes
函数,这种转换不需要额外的内存复制。
转换后的 []byte
类型的切片与原 string
类型的字符串共享底层的内存,而 []byte
变量转 string
类型则更为简单,
通过 unsafe.Pointer
将 []byte
的内部表示重新解释为 string
的内部表示,这就是 BytesToString
的原理
此外 unsafe 在自定义高性能序列化函数(marshal)、原子操作(atomic)及内存操作(指针运算)上都有一定程度的应用。
正确理解 Pointer 与 uintptr
Go 语言内存管理是基于垃圾回收的,垃圾回收例程会定期执行。如果一块内存没有被任何对象引用,它就会被垃圾回收器回收,而对象引用是通过指针来实现的。
unsafe.Pointer
和其他常规类型指针一样,可以作为对象引用。如果一个对象仍然被某个 unsafe.Pointer
引用,那么这个对象就不会被垃圾回收器回收。
但 uintptr
并不是指针,它仅仅是一个整型值,即便它存储的是某个对象的内存地址,它也不会被算作对该对象的引用。如果认为将对象地址存储在一个 uintptr
变量中,
该变量就不会被垃圾回收器回收,那就是对 uintptr
的误解。
使用 uintptr
类型变量保存栈上的变量的地址同样有风险的,因为 Go 使用的是连续栈的栈管理方案,每个 goroutine 的默认栈大小是 2KB(_StackMin = 2048)。
当 goroutine 的栈空间不足时,Go 会自动扩展栈空间,运行时会新分配一块更大的内存空间作为新的栈空间,然后将旧栈空间的内容复制到新的栈空间中。这样原栈上分配的变量地址就会发生变化。