搜索
首页后端开发Golang了解一下Golang中的unsafe包

了解一下Golang中的unsafe包

Apr 02, 2023 am 08:30 AM
go后端

在一些底层的库中, 经常会看到使用 unsafe 包的地方。本篇文章就来带大家了解一下Golang中的unsafe包,介绍一下unsafe 包的作用和Pointer的使用方式,希望对大家有所帮助!

了解一下Golang中的unsafe包

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。

screenshot-20230330-173040.png

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 uintptr

func 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 的六种使用姿势。

  1. 通过 Pointer 将 *T1 转换为 *T2

Pointer 直接指向一块内存, 因此可以将这块内存地址转为任意类型。这里需要注意, 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)
}

screenshot-20230330-173432.png

如果 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, 
}

image.png

  1. 将 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)))    // 不允许
}
  1. 通过算数计算将 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)
}
  1. 当调用 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))

  1. 可以将 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)
}
  1. 可以将 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

image.png

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包的详细内容。更多信息请关注PHP中文网其他相关文章!

声明
本文转载于:掘金社区。如有侵权,请联系admin@php.cn删除
在Golang和Python之间进行选择:适合您的项目在Golang和Python之间进行选择:适合您的项目Apr 19, 2025 am 12:21 AM

golangisidealforperformance-Critical-clitageAppations and ConcurrentPrompromming,而毛皮刺激性,快速播种和可及性。1)forhigh-porformanceneeds,pelectgolangduetoitsefefsefefseffifeficefsefeflicefsiveficefsiveandconcurrencyfeatures.2)fordataa-fordataa-fordata-fordata-driventriventriventriventriventrivendissp pynonnononesp

Golang:并发和行动绩效Golang:并发和行动绩效Apr 19, 2025 am 12:20 AM

Golang通过goroutine和channel实现高效并发:1.goroutine是轻量级线程,使用go关键字启动;2.channel用于goroutine间安全通信,避免竞态条件;3.使用示例展示了基本和高级用法;4.常见错误包括死锁和数据竞争,可用gorun-race检测;5.性能优化建议减少channel使用,合理设置goroutine数量,使用sync.Pool管理内存。

Golang vs. Python:您应该学到哪种语言?Golang vs. Python:您应该学到哪种语言?Apr 19, 2025 am 12:20 AM

Golang更适合系统编程和高并发应用,Python更适合数据科学和快速开发。1)Golang由Google开发,静态类型,强调简洁性和高效性,适合高并发场景。2)Python由GuidovanRossum创造,动态类型,语法简洁,应用广泛,适合初学者和数据处理。

Golang vs. Python:性能和可伸缩性Golang vs. Python:性能和可伸缩性Apr 19, 2025 am 12:18 AM

Golang在性能和可扩展性方面优于Python。1)Golang的编译型特性和高效并发模型使其在高并发场景下表现出色。2)Python作为解释型语言,执行速度较慢,但通过工具如Cython可优化性能。

Golang vs.其他语言:比较Golang vs.其他语言:比较Apr 19, 2025 am 12:11 AM

Go语言在并发编程、性能、学习曲线等方面有独特优势:1.并发编程通过goroutine和channel实现,轻量高效。2.编译速度快,运行性能接近C语言。3.语法简洁,学习曲线平缓,生态系统丰富。

Golang和Python:了解差异Golang和Python:了解差异Apr 18, 2025 am 12:21 AM

Golang和Python的主要区别在于并发模型、类型系统、性能和执行速度。1.Golang使用CSP模型,适用于高并发任务;Python依赖多线程和GIL,适合I/O密集型任务。2.Golang是静态类型,Python是动态类型。3.Golang编译型语言执行速度快,Python解释型语言开发速度快。

Golang vs.C:评估速度差Golang vs.C:评估速度差Apr 18, 2025 am 12:20 AM

Golang通常比C 慢,但Golang在并发编程和开发效率上更具优势:1)Golang的垃圾回收和并发模型使其在高并发场景下表现出色;2)C 通过手动内存管理和硬件优化获得更高性能,但开发复杂度较高。

Golang:云计算和DevOps的关键语言Golang:云计算和DevOps的关键语言Apr 18, 2025 am 12:18 AM

Golang在云计算和DevOps中的应用广泛,其优势在于简单性、高效性和并发编程能力。1)在云计算中,Golang通过goroutine和channel机制高效处理并发请求。2)在DevOps中,Golang的快速编译和跨平台特性使其成为自动化工具的首选。

See all articles

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

AI Hentai Generator

AI Hentai Generator

免费生成ai无尽的。

热工具

mPDF

mPDF

mPDF是一个PHP库,可以从UTF-8编码的HTML生成PDF文件。原作者Ian Back编写mPDF以从他的网站上“即时”输出PDF文件,并处理不同的语言。与原始脚本如HTML2FPDF相比,它的速度较慢,并且在使用Unicode字体时生成的文件较大,但支持CSS样式等,并进行了大量增强。支持几乎所有语言,包括RTL(阿拉伯语和希伯来语)和CJK(中日韩)。支持嵌套的块级元素(如P、DIV),

VSCode Windows 64位 下载

VSCode Windows 64位 下载

微软推出的免费、功能强大的一款IDE编辑器

EditPlus 中文破解版

EditPlus 中文破解版

体积小,语法高亮,不支持代码提示功能

螳螂BT

螳螂BT

Mantis是一个易于部署的基于Web的缺陷跟踪工具,用于帮助产品缺陷跟踪。它需要PHP、MySQL和一个Web服务器。请查看我们的演示和托管服务。

SublimeText3汉化版

SublimeText3汉化版

中文版,非常好用