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

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

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

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

功能

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

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

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

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

    type Person struct {
    age int
    name string
    }
    person := Person{age:18,name:"k哥"}
    p := unsafe.Pointer(&person) // 其它类型指针转Pointer
    u := uintptr(p) // Pointer类型转为uintptr
    u=u+8 // uintptr加减操作
    pName := unsafe.Pointer(u) // uintptr转换为Pointer
    name := *(*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) []ArbitraryType
    func SliceData(slice []ArbitraryType) *ArbitraryType
    func 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语言函数
    }

  2. 高性能类型转换。

    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和架构师技能。

相关推荐
lead520lyq7 小时前
Golang本地内存缓存
开发语言·缓存·golang
小邓吖8 小时前
自己做了一个工具网站
前端·分布式·后端·中间件·架构·golang
金庆15 小时前
Commit Hash from debug.ReadBuildInfo()
golang
源代码•宸16 小时前
Golang面试题库(sync.Map)
开发语言·后端·面试·golang·map·sync.map·expunged
终生成长者17 小时前
Golang cursorrule
开发语言·后端·golang
席万里17 小时前
基于Go和Vue快速开发的博客系统-快速上手Gin框架
vue.js·golang·gin
源代码•宸18 小时前
Golang面试题库(Map)
后端·面试·golang·map·bmap·hmap·nevacuate
只是懒得想了18 小时前
用Go通道实现并发安全队列:从基础到最佳实践
开发语言·数据库·golang·go·并发安全
Fgaoxing1 天前
Go反射:性能瓶颈与零拷贝优化
性能优化·golang·反射
源代码•宸1 天前
Leetcode—94. 二叉树的中序遍历【简单】
经验分享·后端·算法·leetcode·职场和发展·golang·dfs