广告位联系
返回顶部
分享到

golang中unsafe包的实现介绍

Golang 来源:互联网 作者:佚名 发布时间:2026-08-30 17:59:59 人浏览
摘要

unsafe 包 Go 语言中的 unsafe 包是一个非常强大且危险的工具,它允许开发者进行低级的内存操作,直接访问内存地址。通常,Go 是一种安全的语言,它会自动管理内存并做类型检查,确保程序的

unsafe 包

Go 语言中的 unsafe 包是一个非常强大且危险的工具,它允许开发者进行低级的内存操作,直接访问内存地址。通常,Go 是一种安全的语言,它会自动管理内存并做类型检查,确保程序的安全性和稳定性。但是,unsafe 包允许开发者绕过 Go 的类型安全检查,直接操作内存。因为它提供了对底层内存的直接访问,所以在使用时需要非常小心。

Sizeof

1

func Sizeof(x ArbitraryType) uintptr  //ArbitraryType 代表任意类型

在 Go 语言中,unsafe 包提供了一个函数 unsafe.Sizeof,可以用来获取变量或类型的大小(以字节为单位)。

示例:

1

2

3

4

5

6

7

a := 1

b := []int{1, 2, 3}

c := "abc"

 

fmt.Println(unsafe.Sizeof(a)) // 8

fmt.Println(unsafe.Sizeof(b)) // 24

fmt.Println(unsafe.Sizeof(c)) // 16

需要注意的是,unsafe.Sizeof 仅计算变量在内存中的占用大小,不包括动态分配的内存(如切片或映射的底层数据)。

静态内存 VS 动态内存

  • 静态内存占用:指的是类型本身在内存中的固定占用大小。例如,int 类型占 8 字节(在 64 位系统中),struct 的字段的大小,以及 map 或 slice 这种类型的结构体本身占用的内存大小。
  • 动态内存占用:指的是 切片、映射、字符串 等类型在实际使用过程中,可能会从堆上分配更多内存来存储它们的数据。unsafe.Sizeof 仅计算静态内存占用,不会包括这些动态分配的内存区域。

示例:

  1. 切片

1

2

s := []int{1, 2, 3, 4, 5}

   fmt.Println(unsafe.Sizeof(s)) // 24

解释:

切片本身是一个结构体,通常它有三个字段:

  • 一个指向底层数组的指针(8 字节,64 位系统)
  • 当前切片的长度(8 字节,64 位系统)
  • 当前切片的容量(8 字节,64 位系统)

所以,unsafe.Sizeof(s) 输出 24 字节,并没有把底层数组的内存大小包括在内。

如果切片有 5 个整数(每个整数占 8 字节),那么底层数组的内存是 5 * 8 = 40 字节,但这并不会被 unsafe.Sizeof 计算进去。

  1. 字符串

1

2

str := "hello"

fmt.Println(unsafe.Sizeof(str)) //16

解释:

字符串 str 的底层内存是由两个部分组成的:

  • 一个指向底层字节数组的指针(8 字节,64 位系统)
  • 字符串的长度(8 字节,64 位系统)

unsafe.Sizeof(str) 会返回 16 字节(在 64 位系统上),这只是字符串结构体的大小,不包括底层字节数组(即 “hello” 存储的数据)的内存

Offsetof

1

func Offsetof(x ArbitraryType) uintptr

unsafe.Offsetof ,用于获取结构体内某个字段相对于结构体起始地址的偏移量(或者说返回值就是结构体地址起始处到字段地址起始处两者之间的字节数),所以 x 必须是一个结构体的字段。它返回一个 uintptr 类型的值,表示该字段在结构体中的内存偏移位置

示例:

1

2

3

4

5

6

7

8

9

10

11

12

13

type person struct {

    name string

    age  int

}

p := person{

    name: "aa",

    age:  11,

}

fmt.Println(unsafe.Sizeof(p))        //24

fmt.Println(unsafe.Offsetof(p.name)) //0

fmt.Println(unsafe.Sizeof(p.name))   //16

fmt.Println(unsafe.Offsetof(p.age))  //16

fmt.Println(unsafe.Sizeof(p.age))    //8

求结构体占用字节数涉及到内存对齐相关知识,可以看结构体中内存对齐部分

Alignof

1

func Alignof(x ArbitraryType) uintptr

unsafe.Alignof ,用于获取类型或变量对齐数的函数,以字节为单位,取计算机字长(对于64位机器是8)与变量占用字节数的较小值

示例:

1

2

3

4

5

6

7

fmt.Println(unsafe.Alignof([]int{1}))      //slice -- 8

fmt.Println(unsafe.Alignof('a'))           //byte -- 1

fmt.Println(unsafe.Alignof(complex(1, 2))) //complex -- 8

fmt.Println(unsafe.Alignof(0))             //int -- 8

fmt.Println(unsafe.Alignof("abc"))         //string -- 8

fmt.Println(unsafe.Alignof(float64(0)))    //float64 -- 8

fmt.Println(unsafe.Alignof(person{}))      //person -- 8

Pointer

1

type Pointer *ArbitraryType

unsafe.Pointer 类型是一个通用的指针类型,可以指向任何数据类型。

unsafe.Pointer 本身不能直接解引用,因此需要通过类型转换将其转换为其他具体类型的指针。
为什么不能解引用?
在 Go 中,普通的指针(例如 * int, * string 等)都指向某个特定类型的数据,可以通过解引用操作符 * 来访问指针指向的值。然而,unsafe.Pointer 是一个通用指针,不直接指向特定类型的数据,而Go 需要明确知道它指向的数据类型才能安全地进行解引用。所以不能像普通的指针那样通过解引用获取其值,或者直接对它进行取地址操作。

unsafe.Pointer 类型的几种特殊操作:

任何类型的指针都可以转换为 unsafe.Pointer

实现两种类型 *T1、*T2 的转换可以通过 unsafe.Pointer 这一中间量来完成

(1)首先将 *T1 转换成 unsafe.Pointer
(2)再将 unsafe.Pointer 转换成 *T2

示例:

1

2

3

4

5

6

7

8

9

10

11

12

func main() {

   fmt.Println(Float64bits(12.3))

   fmt.Println(Float64frombits(Float64bits(12.3)))

}

 

func Float64bits(f float64) uint64 {

   return *(*uint64)(unsafe.Pointer(&f)) //float64->*float64->unsafe.Pointer->*uint64->uint64

}

 

func Float64frombits(b uint64) float64 {

   return *(*float64)(unsafe.Pointer(&b))//unit64->*unit64->unsafe.Pointer->*float64->float64

}

*T1 和 *T2 的转换有一定要求和规则:

(1)内存布局相同:为了能够安全地将一种类型转换为另一种类型,必须满足这两种类型的内存布局一致。如果是结构体类型,包括:

  • 字段顺序相同
  • 字段大小相同
  • 对齐要求相同

(2) 相同的底层类型:当我们说两种类型的底层类型相同时,指的是它们的数据存储方式必须一致。例如:

  • 基本类型:比如两个都是 int32 类型,或者两个都是 float64 类型,底层存储是相同的。
  • 结构体:结构体类型的内存布局必须完全相同,比如字段的顺序、类型、大小和对齐方式一致。
  • 数组:如果两个数组的类型相同(比如都是 [10]int),它们的内存布局也是相同的。

(3) 不同底层类型不能转换:对于类型不相同的情况(如 int 转 string),即使它们在某些情况下有相似的字节表示(比如在底层都有字节存储),它们的表示和存储方式也不同,因此不能直接通过内存转换进行类型转换。例如:

int 和 string:虽然 string 内部也使用字节存储数据,但它不仅存储字节数据,还有指向数据的指针和长度信息,和 int 的内存布局完全不同,因此不能直接转换。

 int 和 float64:虽然这两者都是数字类型,但它们的二进制表示(内存布局)完全不同,因此也不能直接转换。

将unsafe.Pointer转换为uintptr

unsafe.Pointer 是指针类型
uintptr 不是指针类型,而是一个 无符号整数类型。它可以用来表示 指针的内存地址,但它并不具备指针的特性。简单来说,uintptr 存储的是指针的地址(即整数值),只是一个整数,不具备解引用功能,也不直接表示指向某个数据的引用

示例:

1

2

3

num := 1

fmt.Println(unsafe.Pointer(&num))                 //0xc0000940a8

fmt.Printf("0x%x", uintptr(unsafe.Pointer(&num))) //0xc0000940a8

Add

1

func Add(ptr Pointer, len IntegerType) Pointer

unsafe.Add 是 Go 语言中 unsafe 包提供的一个函数,允许对指针进行偏移操作。具体来说,unsafe.Add 的作用是根据偏移量来更新指针地址,这样可以方便地访问数组、切片或者结构体中的元素。

注意:偏移量是以字节为单位的

示例:

1

2

3

4

s := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}

ps := unsafe.Pointer(&s[0])

fmt.Println(*(*int)(unsafe.Add(ps, 8)))  //2

fmt.Println(*(*int)(unsafe.Add(ps, 16))) //3

解释:

unsafe.Pointer 函数
ps 通过 unsafe.Pointer 获取了指向切片第一个元素的指针,指向 s[0]

unsafe.Add 函数
通过 Add 函数,从 ps 开始,偏移量为 8 个字节,int 类型是 8 个字节,也就是向后移动一个元素,指向 2 ,再将类型转换成 *int,最后解引用

SliceData

1

func SliceData(slice []ArbitraryType) *ArbitraryTyp

SliceData, 获取的是切片的底层数组的指针,而不是切片本身。对于切片来说,底层数组保存了数据的实际内存地址,SliceData 返回的是这个地址。可以使用这个地址来进行内存操作或与其他低级 API 进行交互

示例

1

2

3

4

5

6

7

func main() {

  nums := []int{1, 2, 3, 4}

  for p, i := unsafe.Pointer(&nums[0]), 0; i < len(nums); p, i = unsafe.Add(p, unsafe.Sizeof(nums[0])), i+1 {

    num := *(*int)(p)

    fmt.Println(num)

  }

}

解释:
unsafe.Pointer 函数
将 nums[0](切片的第一个元素)的地址强制转换为 unsafe.Pointer 类型。这样 p 就指向了 nums 切片的第一个元素 nums[0]。

i := 0:i 是索引,初始化为 0,用于跟踪遍历到的切片位置。

unsafe.Add 函数
在 p 的基础上增加 unsafe.Sizeof(nums[0]) 字节,实际是将指针 p 向后移动 4 字节,指向下一个 int 元素

i + 1:每次循环结束后,i 加 1,用于跟踪切片中的索引位置。

Slice

1

func Slice(ptr *ArbitraryType, len IntegerType) []ArbitraryType

Slice ,将指针和长度转换为一个切片

示例:

1

2

3

4

5

6

func main() {

  nums := []int{1, 2, 3, 4}

  numsRef1 := unsafe.Slice(unsafe.SliceData(nums), len(nums))

  numsRef1[0] = 2

  fmt.Println(nums)

}

解释:

unsafe.SliceData 函数
返回 切片 nums 底层数组的起始位置 的首地址

unsafe.Slice 函数
根据给定的底层数组地址和切片长度创建一个新的切片。重要的是,这个新的切片不会进行内存拷贝,而是直接操作底层数组的地址。因此,修改这个新切片的数据会直接影响到原切片的底层数组的数据。

StringData

1

func StringData(str string) *byte

StringData ,类似SliceData,获取字符串的底层数据指针。返回一个 uintptr 类型的值,代表字符串底层数据的内存地址

示例:

1

2

3

4

5

6

7

func main() {

  str := "hello,world!"

  for ptr, i := unsafe.Pointer(unsafe.StringData(str)), 0; i < len(str); ptr, i = unsafe.Add(ptr, unsafe.Sizeof(byte(0))), i+1 {

    char := *(*byte)(ptr)

    fmt.Println(string(char))

  }

}

String

1

func String(ptr *byte, len IntegerType) string

String,类似 Slice,将给定的字节数组(指针和长度)转换为一个 string 类型

示例:

1

2

3

4

5

func main() {

  bytes := []byte("hello world")

  str := unsafe.String(unsafe.SliceData(bytes), len(bytes))

  fmt.Println(str)

}


版权声明 : 本文内容来源于互联网或用户自行发布贡献,该文观点仅代表原作者本人。本站仅提供信息存储空间服务和不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权, 违法违规的内容, 请发送邮件至2530232025#qq.cn(#换@)举报,一经查实,本站将立刻删除。
原文链接 :
相关文章
  • golang中unsafe包的实现介绍
    unsafe 包 Go 语言中的 unsafe 包是一个非常强大且危险的工具,它允许开发者进行低级的内存操作,直接访问内存地址。通常,Go 是一种安全的
  • Linux下Epoll与Go netpoll上下文切换
    Go netpoll 本身不引发上下文切换 你看到的goroutine 阻塞在conn.Read(),不是线程(M)被挂起,而是 goroutine 被gopark挂起、让出 M 给其他 G 使用。
  • rust、go、java、python、nodejs各语言内存对比介绍
    在高负载业务场景中,比如Web服务的高频请求处理、Kafka消息的持续消费、流式计算的实时数据处理,我们常常面临这样的挑战:大量短命对
  • Go中的闭包函数Closure示例
    闭包(Closure)是编程中一个非常重要、但初学者容易晕的概念。它在函数式编程(Functional Programming)中无处不在,Go 语言对它的支持非常强
  • go语言中regexp正则表达式的操作
    Go 语言的regexp包提供了对正则表达式的支持。 正则表达式(regex)是一种字符串搜索模式,用来检查一个字符串是否符合某种特定的模式,
  • Go实现完全静态编译和交叉编译的代码
    Go 语言天生支持跨平台编译,并且其标准库几乎不依赖系统动态库,所以在大多数场景下,它编译出来的二进制文件几乎可以直接丢到任何
  • Go语言编译环境设置教程

    Go语言编译环境设置教程
    Go语言优势 天生支持高并发 可以自由的去控制其并发量,也就是携程,通过go routine关键字就行了。 自动垃圾回收机制 内存的清理 不需要环
  • Go fmt包中Scan获取标准输入方式
    Go fmt包下有三个函数 可以在程序运行过程中获取用户输入。 fmt.Scan:获取输入 fmt.Scanf:获取输入,但是可以指定格式,go会根据格式解析参
  • go中空接口的具体使用
    接口-空接口 1. 什么是空接口? 空接口是特殊形式的接口类型,普通的接口都有方法,而空接口没有定义任何方法口,也因此,我们可以说
  • 快速解除oracle dataguard的方法

    快速解除oracle dataguard的方法
    有些时候,我们为了使oracle dg的standby库另做他用,需要解除oracle dataguard数据同步。我本地因为standby库存储出现故障,导致dg存在问题,故需
  • 本站所有内容来源于互联网或用户自行发布,本站仅提供信息存储空间服务,不拥有版权,不承担法律责任。如有侵犯您的权益,请您联系站长处理!
  • Copyright © 2017-2022 F11.CN All Rights Reserved. F11站长开发者网 版权所有 | 苏ICP备2022031554号-1 | 51LA统计