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

golang,unsafe · 浏览次数 : 0

小编点评

本文将深入探讨 Golang 中的 unsafe 包,包括其功能、原理以及典型应用和高频面试题。unsafe 包为实现灵活操作内存提供了四个主要功能:定义了 Pointer 类型,实现了指针与 void* 的互相转换;定义了 uintptr 类型,实现了指针与 uintptr 的互相转换及加减等算数运算;提供了高性能类型转换函数,如 Slice、SliceData、String 和 StringData 等。 在高性能类型转换方面,unsafe 包通过零拷贝技术实现了高性能类型转换,避免了传统拷贝方式带来的性能开销。典型应用场景包括与操作系统和非 Go 编写的代码通信,以及高性能类型转换。 文章最后提到了高频面试题:uintptr 和 unsafe.Pointer 的区别,以及字符串转成 byte 数组是否发生内存拷贝。希望通过本文的学习,您能够更好地掌握 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和架构师技能。

与golang如何使用指针灵活操作内存?unsafe包原理解析相似的内容:

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

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

golang uuid库介绍

简介: 在现代软件开发中,全球唯一标识符(UUID)在许多场景中发挥着重要的作用。UUID是一种128位的唯一标识符,它能够保证在全球范围内不重复。在Go语言中,我们可以使用第三方库`github.com/google/uuid`来方便地生成UUID。本文将介绍如何使用这个库来生成不同版本的UUID

golang pprof 监控系列(3) —— memory,block,mutex 统计原理

golang pprof 监控系列(3) —— memory,block,mutex 统计原理 大家好,我是蓝胖子。 在上一篇文章 golang pprof监控系列(2) —— memory,block,mutex 使用里我讲解了这3种性能指标如何在程序中暴露以及各自监控的范围。也有提到memory

Golang channel底层是如何实现的?(深度好文)

Go语言为了方便使用者,提供了简单、安全的协程数据同步和通信机制,channel。那我们知道channel底层是如何实现的吗?今天k哥就来聊聊channel的底层实现原理。同时,为了验证我们是否掌握了channel的实现原理,本文也收集了channel的高频面试题,理解了原理,面试题自然不在话下。

golang sync.Map 与使用普通的 map 的区别

使用sync.Map与普通的Go map主要有以下几点区别: 1. 并发安全性 普通map: 在没有外部同步的情况下,不是并发安全的。在多goroutine访问时,如果没有适当的锁或其他同步机制保护,可能会导致数据竞争和未定义行为。 sync.Map: 是并发安全的。它内部实现了必要的同步机制,允许

巧如范金,精比琢玉,一分钟高效打造精美详实的Go语言技术简历(Golang1.18)

研发少闲月,九月人倍忙。又到了一年一度的“金九银十”秋招季,又到了写简历的时节,如果你还在用传统的Word文档寻找模板,然后默默耕耘,显然就有些落后于时代了,本次我们尝试使用云平台flowcv高效打造一份巧如范金、精比琢玉的高品质Golang技术简历。 首先来到云平台:flowcv.com 点击 t

Go 单元测试基本介绍

目录引入一、单元测试基本介绍1.1 什么是单元测试?1.2 如何写好单元测试1.3 单元测试的优点1.4 单元测试的设计原则二、Go语言测试2.1 Go单元测试概要2.2 Go单元测试基本规范2.3 一个简单例子2.3.1 使用Goland 生成测试文件2.3.2 运行单元测试2.3.3 完善测试用

防微杜渐,未雨绸缪,百度网盘(百度云盘)接口API自动化备份上传以及开源发布,基于Golang1.18

奉行长期主义的开发者都有一个共识:对于服务器来说,数据备份非常重要,因为服务器上的数据通常是无价的,如果丢失了这些数据,可能会导致严重的后果,伴随云时代的发展,备份技术也让千行百业看到了其“云基因”的成长与进化,即基于云存储的云备份。 本次我们使用Golang1.18完成百度网盘(百度云盘)接口AP

从源码角度剖析 golang 如何fork一个进程

# 从源码角度剖析 golang 如何fork一个进程 创建一个新进程分为两个步骤,一个是fork系统调用,一个是execve 系统调用,fork调用会复用父进程的堆栈,而execve直接覆盖当前进程的堆栈,并且将下一条执行指令指向新的可执行文件。 在分析源码之前,我们先来看看golang fork

两种解法搞定Swap Nodes in Pairs算法题

最近还是很喜欢用golang来刷算法题,更接近通用算法,也没有像动态脚本语言那些语法糖,真正靠实力去解决问题。 下面这道题很有趣,也是一道链表题目,具体如下: 24. Swap Nodes in Pairs Solved Medium Topics Companies Given a linked