写点什么

golang 如何使用指针灵活操作内存?unsafe 包原理解析

  • 2024-06-22
    广东
  • 本文字数:2385 字

    阅读完需:约 8 分钟

golang如何使用指针灵活操作内存?unsafe包原理解析

Hi 你好,我是 k 哥。一个大厂工作 6 年,还在继续搬砖的后端程序员。


我们都知道,C/C++提供了强大的万能指针 void*,任何类型的指针都可以和万能指针相互转换。并且指针还可以进行加减等算数操作。那么在 Golang 中,是否有类似的功能呢?答案是有的,这就是我们今天要探讨的 unsafe 包。


本文将深入探讨 unsafe 包的功能和原理。同时,我们学习某种东西,一方面是为了实践运用,另一方面则是出于功利性面试的目的。所以,本文还会为大家介绍 unsafe 包的典型应用以及高频面试题。

功能

为了实现灵活操作内存的目的,unsafe 包主要提供了 4 个功能:


  1. 定义了 Pointer 类型,任何类型的指针都可和 Pointer 互相转换,类似于 c 语言中的 void*


var a int = 1p := unsafe.Pointer(&a) // 其它类型指针转Pointerb := (*int)(p) // Pointer类型转其它类型指针fmt.Println(*b) // 输出1
复制代码


  1. 定义了 uintptr 类型,Pointer 和 uintptr 可以互相转换, 从而实现指针的加减等算数运算。


type Person struct {    age int    name string}person := Person{age:18,name:"k哥"}p := unsafe.Pointer(&person) // 其它类型指针转Pointeru := uintptr(p) // Pointer类型转为uintptru=u+8 // uintptr加减操作pName := unsafe.Pointer(u) // uintptr转换为Pointername := *(*string)(pName)fmt.Println(name) // 输出k哥
复制代码


uintptr 是用于指针运算的,它只是一个存储一个 指针地址int 类型,GC 不把 uintptr 当指针,因此, uintptr 类型的目标可能会被回收


  1. 获取任意类型内存对齐、偏移量和内存大小。


func Alignof(x ArbitraryType) uintptr // 内存对齐func Offsetof(x ArbitraryType) uintptr // 内存偏移量func Sizeof(x ArbitraryType) uintptr // 内存大小
复制代码


  • Alignof 返回类型 x 的内存地址对齐值 m,这个类型在内存中的地址必须是 m 的倍数(基于内存读写性能的考虑)。

  • Offsetof 返回结构体成员 x 在内存中的位置离结构体起始处(结构体的第一个字段的偏移量都是 0)的字节数,即偏移量。

  • Sizeof 返回类型 x 所占据的字节数,如果类型 x 结构有指针,Sizeof 不包含 x 指针成员所指向内容的大小。


ArbitraryType 是占位符,golang 编译器在编译时会替换为具体类型


  1. 高性能类型转换。


func Slice(ptr *ArbitraryType, len IntegerType) []ArbitraryTypefunc SliceData(slice []ArbitraryType) *ArbitraryTypefunc String(ptr *byte, len IntegerType) string func StringData(str string) *byte
复制代码


  • Slice 传入任意类型的指针和长度,返回该类型 slice 变量

  • SliceData 传入任意类型的 slice 变量,返回该 slice 底层数组的指针。

  • String 从一个 byte 指针派生出一个指定长度的字符串。

  • StringData 用来获取一个字符串底层字节序列中的第一个 byte 的指针。

高性能类型转换原理

为什么说 Slice、SliceData、String、StringData 是高性能类型转换函数呢?下面我们就来剖析下它们的实现原理。


本文以 String 和 StringData 函数为例,Slice 和 SliceData 函数实现原理类似。在介绍函数实现原理之前,先认识下 string 类型的底层数据结构 StringHeader。string 类型会被 Golang 编译器编译成此结构,其中 Data 是 byte 数组地址,Len 是字符串长度。


type StringHeader struct {        Data uintptr // byte数组地址        Len  int // 字符串长度}
复制代码


String 函数会被 Go 编译成下面的函数实现逻辑。我们可以发现,ptr 指针转换为 string 类型,是直接将 ptr 赋值给 StringHeader 的成员 Data,而不需要重新拷贝 ptr 指向的 byte 数组。从而通过零拷贝实现高性能类型转换。


import (    "fmt"    "reflect"    "unsafe")
func String(ptr *byte, len int) string { p := (uintptr)(unsafe.Pointer(ptr)) hdr := &reflect.StringHeader{ Data: p, Len: len, } // 将 StringHeader 转为 string str := *(*string)(unsafe.Pointer(hdr)) return str}
func main() { bytes := []byte{'h', 'e', 'l', 'l', 'o'} ptr := &bytes[0] len := 5 str := String(ptr, len) fmt.Println(str) // 输出hello}
复制代码


StringData 函数会被 Go 编译成下面的函数实现逻辑。同理,我们可以发现,string 类型转换为 byte,是直接取 StringHeader 的 uintptr 类型成员 Data,并将其转换为 byte。不需要拷贝整个 string,重新生成 byte 数组。从而通过零拷贝实现高性能类型转换。


import (    "fmt"    "reflect"    "unsafe")
func StringData(str string) *byte { hdr := (*reflect.StringHeader)(unsafe.Pointer(&str)) data := hdr.Data return (*byte)(unsafe.Pointer(data))}
func main() { str := "hello" data := StringData(str) fmt.Println(string(*data)) // 输出h}
复制代码


回到问题,为什么说 Slice、SliceData、String、StringData 是高性能类型转换函数呢?通过 String 和 StringData 函数的实现逻辑,我们可以知道,String 和 StringData 利用 unsafe 包,通过零拷贝,实现了高性能类型转换。

典型应用

在实践中,常见使用 unsafe 包的场景有 2 个:


  1. 与操作系统以及非 go 编写(cgo)的代码通信。


func SetData(bytes []byte) {     cstr := (*C.char)(unsafe.Pointer(&bytes[0])) // 转换成一个C char类型    C.setData(cstr, (C.int)(len(bytes))) // 调用C语言函数}
复制代码


  1. 高性能类型转换。


func Bytes2String(b []byte) string {    return *(*string)(unsafe.Pointer(&b))}
func String2Bytes(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))}
复制代码

高频面试题

  1. 能说说 uintptr 和 unsafe.Pointer 的区别吗?

  2. 字符串转成 byte 数组,会发生内存拷贝吗?


欢迎大家关注我的公粽号【golang 架构师 k 哥】,每周分享 golang 和架构师技能。

发布于: 刚刚阅读数: 5
用户头像

腾讯等大厂6年golang架构师 2018-11-17 加入

我是k哥。大厂6年golang架构师,亿万dau、百万qps服务owner。 公粽号【golang架构师k哥】每周分享golang和架构师技能。

评论

发布
暂无评论
golang如何使用指针灵活操作内存?unsafe包原理解析_golang_golang架构师k哥_InfoQ写作社区