• 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
    
    1. 定义了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语言函数
    }
    
    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和架构师技能。

  • 相关阅读:
    【Pycharm中python调用另一个文件类或者函数】
    利用NVIDIA DALI读取视频帧
    Java——》线程的打断(停止正在运行的线程)
    就只说 3 个 Java 面试题 —— 02
    robotframework在Jenkins执行踩坑
    【Linux权限:系统中的数字锁与安全之门】
    测试工具(三)Git常用命令
    轻松通过 Excel 消费可信指标数据
    dpkt 处理linux cooked capture
    Insight h2database 执行计划评估以及 Selectivity
  • 原文地址:https://www.cnblogs.com/killianxu/p/18262567