您好,欢迎访问一九零五行业门户网

了解一下Golang中的unsafe包

在一些底层的库中, 经常会看到使用 unsafe 包的地方。本篇文章就来带大家了解一下golang中的unsafe包,介绍一下unsafe 包的作用和pointer的使用方式,希望对大家有所帮助!
unsafe 包提供了一些操作可以绕过 go 的类型安全检查, 从而直接操作内存地址, 做一些 tricky 操作。示例代码运行环境是 go version go1.18 darwin/amd64
内存对齐unsafe 包提供了 sizeof 方法获取变量占用内存大小「不包含指针指向变量的内存大小」, alignof 获取内存对齐系数, 具体内存对齐规则可以自行 google.
type demo1 struct { a bool // 1 b int32 // 4 c int64 // 8}type demo2 struct { a bool // 1 c int64 // 8 b int32 // 4}type demo3 struct { // 64 位操作系统, 字长 8 a *demo1 // 8 b *demo2 // 8}func memalign() { fmt.println(unsafe.sizeof(demo1{}), unsafe.alignof(demo1{}), unsafe.alignof(demo1{}.a), unsafe.alignof(demo1{}.b), unsafe.alignof(demo1{}.c)) // 16,8,1,4,8 fmt.println(unsafe.sizeof(demo2{}), unsafe.alignof(demo2{}), unsafe.alignof(demo2{}.a), unsafe.alignof(demo2{}.b), unsafe.alignof(demo2{}.c)) // 24,8,1,4,8 fmt.println(unsafe.sizeof(demo3{})) // 16} // 16}复制代码
从上面 case 可以看到 demo1 和 demo2 包含相同的属性, 只是定义的属性顺序不同, 却导致变量的内存大小不同。这里是因为发生了内存对齐。
计算机在处理任务时, 会按照特定的字长「例如:32 位操作系统, 字长为 4; 64 位操作系统, 字长为 8」为单位处理数据。那么, 在读取数据的时候也是按照字长为单位。例如: 对于 64 位操作系统, 程序一次读取的字节数为 8 的倍数。下面是 demo1 在非内存对齐和内存对齐下的布局:
非内存对齐:
变量 c 会被放在不同的字长里面, cpu 在读取的时候需要同时读取两次, 同时对两次的结果做处理, 才能拿到 c 的值。这种方式虽然节省了内存空间, 但是会增加处理时间。
内存对齐:
内存对齐采用了一种方案, 可以避免同一个非内存对齐的这种情况, 但是会额外占用一些空间「空间换时间」。具体内存对齐规则可以自行 google。
unsafe pointer在 go 中可以声明一个指针类型, 这里的类型是 safe pointer, 即要明确指针指向的类型, 如果类型不匹配将会在编译时报错。如下面的示例, 编译器会认为 mystring 和 string 是不同的类型, 无法进行赋值。
func main() { type mystring string s := "test" var ms mystring = s // cannot use 's' (type string) as the type mystring fmt.println(ms)}
那有没有一种类型, 可以指向任意类型的变量呢?可以使用 unsfe.pointer, 它可以指向任意类型的变量。通过pointer 的声明, 可以知道它是一个指针类型, 指向变量所在的地址。具体的地址对应的值可以通过 uinptr 进行转换。pointer 有以下四种特殊的操作:
任意类型的指针都可以转换成 pointer 类型pointer 类型的变量可以转换成任意类型的指针uintptr 类型的变量可以转换成 pointer 类型pointer 类型的变量可以转换成 uintprt 类型type pointer *arbitrarytype// uintptr is an integer type that is large enough to hold the bit pattern of// any pointer.type uintptr uintptrfunc main() { d := demo1{true, 1, 2} p := unsafe.pointer(&d) // 任意类型的指针可以转换为 pointer 类型 pa := (*demo1)(p) // pointer 类型变量可以转换成 demo1 类型的指针 up := uintptr(p) // pointer 类型的变量可以转换成 uintprt 类型 pu := unsafe.pointer(up) // uintptr 类型的变量可以转换成 pointer 类型; 当 gc 时, d 的地址可能会发生变更, 因此, 这里的 up 可能会失效 fmt.println(d.a, pa.a, (*demo1)(pu).a) // true true true}
pointer 的六种使用方式在官方文档中给出了 pointer 的六种使用姿势。
通过 pointer 将 *t1 转换为 *t2pointer 直接指向一块内存, 因此可以将这块内存地址转为任意类型。这里需要注意, t1 和 t2 需要有相同的内存布局, 会有异常数据。
func main() { type mystr string ms := []mystr{"1", "2"} //ss := ([]string)(ms) cannot convert an expression of the type '[]mystr' to the type '[]string' ss := *(*[]string)(unsafe.pointer(&ms)) // 将 pointer 指向的内存地址直接转换成 *[]string fmt.println(ms, ss)}
如果 t1 和 t2 的内存布局不同, 会发生什么呢?在下面的示例子中, demo1 和 demo2 虽然包含相同的结构体, 由于内存对齐, 导致两者是不同的内存布局。将 pointer 转换时, 会从 demo1 的地址开始读取 24「sizeof」 个字节, 按照demo2 内存对齐规则进行转换, 将第一个字节转换为 a:true, 8-16 个字节转换为 c:2, 16-24 个字节超出了 demo1 的范围, 但仍可以直接读取, 获取了非预期的值 b:17368000。
type demo1 struct { a bool // 1 b int32 // 4 c int64 // 8}type demo2 struct { a bool // 1 c int64 // 8 b int32 // 4}func main() { d := demo1{true, 1, 2} pa := (*demo2)(unsafe.pointer(&d)) // pointer 类型变量可以转换成 demo2 类型的指针 fmt.println(pa.a, pa.b, pa.c) // true, 17368000, 2, }
将 pointer 类型转换为 uintptr 类型「不应该将 uinptr 转为 pointer」pointer 是一个指针类型, 可以指向任意变量, 可以通过将 pointer 转换为 uintptr 来打印 pointer 指向变量的地址。此外:不应该将 uintptr 转换为 pointer。如下面的例子: 当发生 gc 时, d 的地址可能会发生变更, 那么 up 由于未同步更新而指向错误的内存。
func main() { d := demo1{true, 1, 2} p := unsafe.pointer(&d) up := uintptr(p) fmt.printf("uintptr: %x, ptr: %p \n", up, &d) // uintptr: c00010c010, ptr: 0xc00010c010 fmt.println(*(*demo1)(unsafe.pointer(up))) // 不允许}
通过算数计算将 pointer 转换为 uinptr 再转换回 pointer当 piointer 指向一个结构体时, 可以通过此方式获取到结构体内部特定属性的 pointer。
func main() { d := demo1{true, 1, 2} // 等同于 unsafe.pointer(&d.b); unsafe.add(unsafe.pointer(&d), unsafe.offsetof(d.b)) pb := unsafe.pointer(uintptr(unsafe.pointer(&d)) + unsafe.offsetof(d.b)) fmt.println(pb)}
当调用 syscall.syscall 的时候, 可以讲 pointer 转换为 uintptr前面说过, 由于 gc 会导致变量的地址发生变更, 因此不可以直接处理 uintptr。但是, 在调用 syscall.syscall 时候可以允许传递一个 uintptr, 这里可以简单理解为是编译器做了特殊处理, 来保证 uintptr 是安全的。
调用方式: syscall.syscall(sys_read, uintptr( fd ), uintptr(unsafe.pointer(p)), uintptr(n))下面这种方式是不允许的:
u := uintptr(unsafe.pointer(p)) // 不应该保存到一个变量上 syscall.syscall(sys_read, uintptr( fd ), u, uintptr(n))
可以将 reflect.value.pointer 或 reflect.value.unsafeaddr 的结果「uintptr」转换为 pointer在 reflect 包中的 value.pointer 和 value.unsafeaddr 直接返回了地址对应的值「uintptr」, 可以直接将其结果转为 pointer
func main() { d := demo1{true, 1, 2} // 等同于 unsafe.pointer(&d.b); unsafe.add(unsafe.pointer(&d), unsafe.offsetof(d.b)) pb := unsafe.pointer(uintptr(unsafe.pointer(&d)) + unsafe.offsetof(d.b)) // up := reflect.valueof(&d.b).pointer(), pc := unsafe.pointer(up); 不安全, 不应存储到变量中 pc := unsafe.pointer(reflect.valueof(&d.b).pointer()) fmt.println(pb, pc)}
可以将 reflect.sliceheader 或者 reflect.stringheader 的 data 字段与 pointer 相互转换sliceheader 和 stringheader 其实是 slice 和 string 的内部实现, 里面都包含了一个字段 data「uintptr」, 存储的是指向 []t 的地址, 这里之所以使用 uinptr 是为了不依赖 unsafe 包。
func main() { s := "a" hdr := (*reflect.stringheader)(unsafe.pointer(&s)) // *string to *stringheader fmt.println(*(*[1]byte)(unsafe.pointer(hdr.data))) // 底层存储的是 utf 编码后的 byte 数组 arr := [1]byte{65} hdr.data = uintptr(unsafe.pointer(&arr)) hdr.len = len(arr) ss := *(*string)(unsafe.pointer(hdr)) fmt.println(ss) // a arr[0] = 66 fmt.println(ss) //b}
应用string、byte 转换在业务上, 经常遇到 string 和 []byte 的相互转换。我们知道, string 底层其实也是存储的一个 byte 数组, 可以通过 reflect 直接获取 string 指向的 byte 数组, 赋值给 byte 切片, 避免内存拷贝。
func strtobyte(str string) []byte { return []byte(str)}func strtobytev2(str string) (b []byte) { bh := (*reflect.sliceheader)(unsafe.pointer(&b)) sh := (*reflect.stringheader)(unsafe.pointer(&str)) bh.data = sh.data bh.cap = sh.len bh.len = sh.len return b}// go test -bench .func benchmarkstrtoarr(b *testing.b) { for i := 0; i < b.n; i++ { strtobyte(`{"f": "v"}`) }}func benchmarkstrtoarrv2(b *testing.b) { for i := 0; i < b.n; i++ { strtobytev2(`{"f": "v"}`) }}//goos: darwin//goarch: amd64//pkg: github.com/demo/lsafe//cpu: intel(r) core(tm) i7-9750h cpu @ 2.60ghz//benchmarkstrtoarr-12 264733503 4.311 ns/op//benchmarkstrtoarrv2-12 1000000000 0.2528 ns/op
通过观察 string 和 byte 的内存布局我们可以知道, 无法直接将 string 转为 []byte 「确实 cap 字段」, 但是可以直接将 []byte 转为 string
func bytetostr(b []byte) string { return string(b)}func bytetostrv2(b []byte) string { return *(*string)(unsafe.pointer(&b))}// go test -bench .func benchmarkarrtostr(b *testing.b) { for i := 0; i < b.n; i++ { bytetostr([]byte{65}) }}func benchmarkarrtostrv2(b *testing.b) { for i := 0; i < b.n; i++ { bytetostrv2([]byte{65}) }}//goos: darwin//goarch: amd64//pkg: github.com/demo/lsafe//cpu: intel(r) core(tm) i7-9750h cpu @ 2.60ghz//benchmarkarrtostr-12 536188455 2.180 ns/op//benchmarkarrtostrv2-12 1000000000 0.2526 ns/op
总结本文介绍了如何使用 unsafe 包绕过类型检查, 直接操作内存。正如 go 作者对包的命名一样, 它是 unsafe 的, 随着 go 版本的迭代, 有些机制可能会发生变更。如无必要, 不应使用这个包。如果要使用 unsafe 包, 一定要理解清楚pointer、uinptr、对齐系数等概念。
推荐学习:golang教程
以上就是了解一下golang中的unsafe包的详细内容。
其它类似信息

推荐信息