我的编程空间,编程开发者的网络收藏夹
学习永远不晚

了解一下Golang中的unsafe包

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

了解一下Golang中的unsafe包

在一些底层的库中, 经常会看到使用 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中的unsafe包的详细内容,更多请关注编程网其它相关文章!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

了解一下Golang中的unsafe包

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

Golang中的unsafe包有什么用

今天小编给大家分享一下Golang中的unsafe包有什么用的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。unsafe 包提
2023-07-05

了解一下 Golang 架构中的关键要素吧!

了解一下 Golang 架构中的关键要素吧!Golang(或称为Go语言)是一种由Google开发的编程语言,它被设计用来提高程序员的生产力。Golang具有简洁的语法和高效的编译器,适合用于构建大型、高性能的软件系统。在Golang的架
了解一下 Golang 架构中的关键要素吧!
2024-03-02

一文带你了解Golang中的WaitGroups

WaitGroups是同步你的goroutines的一种有效方式。这篇文章主要来和大家聊聊Golang中WaitGroups的使用,感兴趣的小伙伴可以跟随小编一起了解一下
2023-03-14

一文带你了解Golang中的并发性

并发是一个很酷的话题,一旦你掌握了它,就会成为一笔巨大的财富。所以本文就来和大家一起来聊聊Golang中的并发性,感兴趣的可以了解一下
2023-03-15

哪个是最好的Golang版本需要了解一下?

了解一下Golang最好的版本是哪个?随着科技的发展和编程语言的不断更新换代,开发者们总是追寻着最好的版本来提高编程的效率和性能。Golang(Go语言)作为一门类似C语言,但更加简洁和高效的编程语言,也有着自己的版本选择。Golang
哪个是最好的Golang版本需要了解一下?
2024-01-20

Golang Bee 究竟是什么?深入了解一下

Golang Bee 究竟是什么?深入了解一下,需要具体代码示例随着 Go 语言在开发领域的日益流行,Golang Bee 作为一个快速且强大的 Web 开发框架也逐渐走进了开发者的视野。那么,Golang Bee究竟是什么呢?本文将带您
Golang Bee 究竟是什么?深入了解一下
2024-03-06

Golang学习是否需要花钱?了解一下!

随着互联网信息的爆炸性增长,编程语言的种类也日益增多,每一种编程语言都有自己的特点和应用领域。Golang作为一种开源的编程语言,受到了许多程序员的喜欢和青睐。对于那些有意学习Golang的人来说,是否需要花钱才能学习呢?这是一个十分常见的
Golang学习是否需要花钱?了解一下!
2024-03-06

一文带你了解Golang中的缓冲区Buffer

作为一种常见的数据结构,缓冲区(Buffer)在计算机科学中有着广泛的应用。这篇文章将详细介绍 Go 中 Buffer 的用法,从多个方面介绍其特性和应用场景,需要的可以参考一下
2023-05-18

一文带你了解Golang中interface的设计与实现

本文就来详细说说为什么说 接口本质是一种自定义类型,以及这种自定义类型是如何构建起 go 的 interface 系统的,感兴趣的小伙伴可以跟随小编一起学习一下
2023-01-04

一文带你了解Golang中reflect反射的常见错误

go 反射的错误大多数都来自于调用了一个不适合当前类型的方法, 而且,这些错误通常是在运行时才会暴露出来,而不是在编译时,如果我们传递的类型在反射代码中没有被覆盖到那么很容易就会 panic。本文就介绍一下使用 go 反射时很大概率会出现的错误,需要的可以参考一下
2023-01-05

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录