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

 更新时间:2024年07月06日 09:23:36   作者:golang架构师k哥  
本文将深入探讨unsafe包的功能和原理,同时,我们学习某种东西,一方面是为了实践运用,另一方面则是出于功利性面试的目的,所以,本文还会为大家介绍unsafe 包的典型应用以及高频面试题,感兴趣的朋友跟随小编一起看看吧

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

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

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

功能

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

  • 定义了Pointer类型,任何类型的指针都可和Pointer互相转换,类似于c语言中的void*
var a int = 1
p := unsafe.Pointer(&a) // 其它类型指针转Pointer
b := (*int)(p) // Pointer类型转其它类型指针
fmt.Println(*b) // 输出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 类型的目标可能会被回收

  • 获取任意类型内存对齐、偏移量和内存大小。
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编译器在编译时会替换为具体类型

  • 高性能类型转换。
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个:

  • 与操作系统以及非go编写(cgo)的代码通信。
func SetData(bytes []byte) { 
    cstr := (*C.char)(unsafe.Pointer(&bytes[0])) // 转换成一个C char类型
    C.setData(cstr, (C.int)(len(bytes))) // 调用C语言函数
}
  • 高性能类型转换。
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))
}

高频面试题

  • 能说说uintptr和unsafe.Pointer的区别吗?
  • 字符串转成byte数组,会发生内存拷贝吗?

到此这篇关于golang如何使用指针灵活操作内存?unsafe包原理解析的文章就介绍到这了,更多相关golang unsafe包原内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Gin框架令牌桶限流实战指南

    Gin框架令牌桶限流实战指南

    限流是一种通过控制请求处理速率来保护系统的技术,它能有效防止服务器因突发流量或恶意攻击而过载,确保服务的稳定性和可用性,本文就来介绍一下Gin 框架令牌桶限流的实现,感兴趣的可以了解一下
    2025-10-10
  • go实现冒泡排序的示例代码

    go实现冒泡排序的示例代码

    这篇文章主要介绍了go实现冒泡排序的示例代码,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-01-01
  • 利用golang实现pdf中自动换行的表格

    利用golang实现pdf中自动换行的表格

    这篇文章主要给大家介绍了如何利用golang实现pdf中自动换行的表格,文中通过代码示例给大家讲解的非常详细,对大家的学习或工作有一定的帮助,需要的朋友可以参考下
    2024-02-02
  • 深入详解如何使用Go实现端口扫描器

    深入详解如何使用Go实现端口扫描器

    这篇文章主要为大家详细介绍了深如何使用Go语言实现一个简单的端口扫描器,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下
    2025-11-11
  • prometheus client_go为应用程序自定义监控指标

    prometheus client_go为应用程序自定义监控指标

    这篇文章主要为大家介绍了prometheus client_go为应用程序自定义监控指标详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-02-02
  • 在Golang中实现定时任务的几种高效方法

    在Golang中实现定时任务的几种高效方法

    本文将详细介绍在Golang中实现定时任务的几种高效方法,包括time包中的Ticker和Timer、第三方库cron的使用,以及基于channel和goroutine的自定义实现,我们将通过实际代码示例和性能分析,帮助开发者选择最适合自己场景的定时任务解决方案,需要的朋友可以参考下
    2025-06-06
  • 使用goland调试远程代码的操作步骤

    使用goland调试远程代码的操作步骤

    大家都知道如何在goland调试远程代码吗?今天小编给大家分享一篇教程帮助大家学习goland调试远程代码的操作步骤,感兴趣的朋友跟随小编一起看看吧
    2021-06-06
  • GO语言实现日志切割的示例详解

    GO语言实现日志切割的示例详解

    日志记录对程序排查问题比较关键,所以本文将选择Logrus和lumberjack两个库进行日志切割以及记录调用源,感兴趣的小伙伴可以了解一下
    2023-07-07
  • 基于Go语言实现应用IP防火墙

    基于Go语言实现应用IP防火墙

    在公司里面经常会听到某应用有安全漏洞问题,没有做安全加固,IP防火墙就是一个典型的安全加固解决方案,下面我们就来学习一下如何使用go语言实现IP防火墙吧
    2023-11-11
  • Go语言学习之数组的用法详解

    Go语言学习之数组的用法详解

    数组是相同数据类型的一组数据的集合,数组一旦定义长度不能修改,数组可以通过下标(或者叫索引)来访问元素。本文将通过示例详细讲解Go语言中数组的使用,需要的可以参考一下
    2022-04-04

最新评论