Go语言之 unsafe 包之内存布局

简介: unsafe,顾名思义,是不安全的,Go定义这个包名也是这个意思,让我们尽可能的不要使用它,如果你使用它,看到了这个名字,也会想到尽可能的不要使用它,或者更小心的使用它。虽然这个包不安全,但是它也有它的优势,那就是可以绕过Go的内存安全机制,直接对内存进行读写,所以有时候因为性能的需要,会冒一些风险使用该包,对内存进行操作。

unsafe,顾名思义,是不安全的,Go定义这个包名也是这个意思,让我们尽可能的不要使用它,如果你使用它,看到了这个名字,也会想到尽可能的不要使用它,或者更小心的使用它。

虽然这个包不安全,但是它也有它的优势,那就是可以绕过Go的内存安全机制,直接对内存进行读写,所以有时候因为性能的需要,会冒一些风险使用该包,对内存进行操作。

Sizeof函数

Sizeof函数可以返回一个类型所占用的内存大小,这个大小只有类型有关,和类型对应的变量存储的内容大小无关,比如bool型占用一个字节、int8也占用一个字节。

func main() {
    fmt.Println(unsafe.Sizeof(true))
    fmt.Println(unsafe.Sizeof(int8(0)))
    fmt.Println(unsafe.Sizeof(int16(10)))
    fmt.Println(unsafe.Sizeof(int32(10000000)))
    fmt.Println(unsafe.Sizeof(int64(10000000000000)))
    fmt.Println(unsafe.Sizeof(int(10000000000000000)))
}

对于整型来说,占用的字节数意味着这个类型存储数字范围的大小,比如int8占用一个字节,也就是8bit,所以它可以存储的大小范围是-128~~127,也就是−2^(n-1)到2^(n-1)−1,n表示bit,int8表示8bit,int16表示16bit,其他以此类推。

对于和平台有关的int类型,这个要看平台是32位还是64位,会取最大的。比如我自己测试,以上输出,会发现int和int64的大小是一样的,因为我的是64位平台的电脑。

func Sizeof(x ArbitraryType) uintptr

以上是Sizeof的函数定义,它接收一个ArbitraryType类型的参数,返回一个uintptr类型的值。这里的ArbitraryType不用关心,他只是一个占位符,为了文档的考虑导出了该类型,但是一般不会使用它,我们只需要知道它表示任何类型,也就是我们这个函数可以接收任意类型的数据。

// ArbitraryType is here for the purposes of documentation only and is not actually// part of the unsafe package. It represents the type of an arbitrary Go expression.
type ArbitraryType int

Alignof 函数

Alignof返回一个类型的对齐值,也可以叫做对齐系数或者对齐倍数。对齐值是一个和内存对齐有关的值,合理的内存对齐可以提高内存读写的性能,关于内存对齐的知识可以参考相关文档,这里不展开介绍。

func main() {
   var b bool    var i8 int8    var i16 int16    var i64 int64    var f32 float32    var s string    var m map[string]string    var p *int32    fmt.Println(unsafe.Alignof(b))    fmt.Println(unsafe.Alignof(i8))    fmt.Println(unsafe.Alignof(i16))    fmt.Println(unsafe.Alignof(i64))    fmt.Println(unsafe.Alignof(f32))    fmt.Println(unsafe.Alignof(s))    fmt.Println(unsafe.Alignof(m))    fmt.Println(unsafe.Alignof(p)) }

从以上例子的输出,可以看到,对齐值一般是2^n,最大不会超过8(原因见下面的内存对齐规则)。Alignof的函数定义和Sizeof基本上一样。这里需要注意的是每个人的电脑运行的结果可能不一样,大同小异。

func Alignof(x ArbitraryType) uintptr

此外,获取对齐值还可以使用反射包的函数,也就是说:unsafe.Alignof(x)等价于reflect.TypeOf(x).Align()

Offsetof 函数

Offsetof函数只适用于struct结构体中的字段相对于结构体的内存位置偏移量。结构体的第一个字段的偏移量都是0.

func main() {
   var u1 user1    fmt.Println(unsafe.Offsetof(u1.b))    fmt.Println(unsafe.Offsetof(u1.i))    fmt.Println(unsafe.Offsetof(u1.j)) }
type user1 struct {    b byte    i int32    j int64
}

字段的偏移量,就是该字段在struct结构体内存布局中的起始位置(内存位置索引从0开始)。根据字段的偏移量,我们可以定位结构体的字段,进而可以读写该结构体的字段,哪怕他们是私有的,黑客的感觉有没有。偏移量的概念,我们会在下一小结详细介绍。

此外,unsafe.Offsetof(u1.i)等价于reflect.TypeOf(u1).Field(i).Offset

有意思的struct大小

我们定义一个struct,这个struct有3个字段,它们的类型有byte,int32以及int64,但是这三个字段的顺序我们可以任意排列,那么根据顺序的不同,一共有6种组合。

type user1 struct {
    b byte
    i int32
    j int64
}
type user2 struct {    b byte    j int64    i int32
}
type user3 struct {    i int32    b byte    j int64
}
type user4 struct {    i int32    j int64    b byte
}
type user5 struct {    j int64    b byte    i int32
}
type user6 struct {    j int64    i int32    b byte
}

根据这6种组合,定义了6个struct,分别位user1,user2,…,user6,那么现在大家猜测一下,这6种类型的struct占用的内存是多少,就是unsafe.Sizeof()的值。

大家可能猜测1+4+8=13,因为byte的大小为1,int32大小为4,int64大小为8,而struct其实就是一个字段的组合,所以猜测struct大小为字段大小之和也很正常。

但是,但是,我可以明确的说,这是错误的。

为什么是错误的,因为有内存对齐存在,编译器使用了内存对齐,那么最后的大小结果就不一样了。现在我们正式验证下,这几种struct的值。

func main() {
   var u1 user1    
   var u2 user2    
   var u3 user3    
   var u4 user4    
   var u5 user5    
   var u6 user6    fmt.Println("u1 size is ",unsafe.Sizeof(u1))    fmt.Println("u2 size is ",unsafe.Sizeof(u2))    fmt.Println("u3 size is ",unsafe.Sizeof(u3))    fmt.Println("u4 size is ",unsafe.Sizeof(u4))    fmt.Println("u5 size is ",unsafe.Sizeof(u5))    fmt.Println("u6 size is ",unsafe.Sizeof(u6)) }

从以上输出可以看到,结果是:

u1 size is  16
u2 size is  24
u3 size is  16
u4 size is  24
u5 size is  16
u6 size is  16

结果出来了(我的电脑的结果,Mac64位,你的可能不一样),4个16字节,2个24字节,既不一样,又不相同,这说明:

  1. 内存对齐影响struct的大小

  2. struct的字段顺序影响struct的大小

综合以上两点,我们可以得知,不同的字段顺序,最终决定struct的内存大小,所以有时候合理的字段顺序可以减少内存的开销

内存对齐会影响struct的内存占用大小,现在我们就详细分析下,为什么字段定义的顺序不同会导致struct的内存占用不一样。

在分析之前,我们先看下内存对齐的规则:

  1. 对于具体类型来说,对齐值=min(编译器默认对齐值,类型大小Sizeof长度)。也就是在默认设置的对齐值和类型的内存占用大小之间,取最小值为该类型的对齐值。我的电脑默认是8,所以最大值不会超过8.

  2. struct在每个字段都内存对齐之后,其本身也要进行对齐,对齐值=min(默认对齐值,字段最大类型长度)。这条也很好理解,struct的所有字段中,最大的那个类型的长度以及默认对齐值之间,取最小的那个。

以上这两条规则要好好理解,理解明白了才可以分析下面的struct结构体。在这里再次提醒,对齐值也叫对齐系数、对齐倍数,对齐模数。这就是说,每个字段在内存中的偏移量是对齐值的倍数即可

我们知道byte,int32,int64的对齐值分别为1,4,8,占用内存大小也是1,4,8。那么对于第一个structuser1,它的字段顺序是byte、int32、int64,我们先使用第1条内存对齐规则进行内存对齐,其内存结构如下,内存布局中有竖线(|),用于每四个字节的分割,下同。

bxxx|iiii|jjjj|jjjj

user1类型,第1个字段byte,对齐值1,大小1,所以放在内存布局中的第1位。

第2个字段int32,对齐值4,大小4,所以它的内存偏移值必须是4的倍数,在当前的user1中,就不能从第2位开始了,必须从第5位开始,也就是偏移量为4。第2,3,4位由编译器进行填充,一般为值0,也称之为内存空洞。所以第5位到第8位为第2个字段i。

第3字段,对齐值为8,大小也是8。因为user1前两个字段已经排到了第8位,所以下一位的偏移量正好是8,是第3个字段对齐值的倍数,不用填充,可以直接排列第3个字段,也就是从第9位到第16位为第3个字段j。

现在第一条内存对齐规则后,内存长度已经为16个字节,我们开始使用内存的第2条规则进行对齐。根据第二条规则,默认对齐值8,字段中最大类型长度也是8,所以求出结构体的对齐值位8,我们目前的内存长度为16,是8的倍数,已经实现了对齐。

所以到此为止,结构体user1的内存占用大小为16字节。

现在我们再分析一个user2类型,它的大小是24,只是调换了一下字段i和j的顺序,就多占用了8个字节,我们看看为什么?还是先使用我们的内存第1条规则分析。

bxxx|xxxx|jjjj|jjjj|iiii

按对齐值和其占用的大小,第1个字段b偏移量为0,占用1个字节,放在第1位。

第2个字段j,是int64,对齐值和大小都是8,所以要从偏移量8开始,也就是第9到16位为j,这也就意味着第2到8位被编译器填充。

目前整个内存布局已经偏移了16位,正好是第3个字段i的对齐值4的倍数,所以不用填充,可以直接排列,第17到20位为i。

现在所有字段对齐好了,整个内存大小为1+7+8+4=20个字节,我们开始使用内存对齐的第2条规则,也就是结构体的对齐,通过默认对齐值和最大的字段大小,求出结构体的对齐值为8。

现在我们的整个内存布局大小为20,不是8的倍数,所以我们需要进行内存填充,补足到8的倍数,最小的就是24,所以对齐后整个内存布局为

bxxx|xxxx|jjjj|jjjj|iiii|xxxx

所以这也是为什么我们最终获得的user2的大小为24的原因。
基于以上办法,我们可以得出其他几个struct的内存布局。

user3

iiii|bxxx|jjjj|jjjj

user4

iiii|xxxx|jjjj|jjjj|bxxx|xxxx

user5

jjjj|jjjj|bxxx|iiii

user6

jjjj|jjjj|iiii|bxxx

以上给出了答案,推到过程大家可以参考user1user2试试。


目录
相关文章
|
21天前
|
存储 监控 算法
员工上网行为监控中的Go语言算法:布隆过滤器的应用
在信息化高速发展的时代,企业上网行为监管至关重要。布隆过滤器作为一种高效、节省空间的概率性数据结构,适用于大规模URL查询与匹配,是实现精准上网行为管理的理想选择。本文探讨了布隆过滤器的原理及其优缺点,并展示了如何使用Go语言实现该算法,以提升企业网络管理效率和安全性。尽管存在误报等局限性,但合理配置下,布隆过滤器为企业提供了经济有效的解决方案。
67 8
员工上网行为监控中的Go语言算法:布隆过滤器的应用
|
1月前
|
存储 Go 索引
go语言中数组和切片
go语言中数组和切片
44 7
|
1月前
|
Go 开发工具
百炼-千问模型通过openai接口构建assistant 等 go语言
由于阿里百炼平台通义千问大模型没有完善的go语言兼容openapi示例,并且官方答复assistant是不兼容openapi sdk的。 实际使用中发现是能够支持的,所以自己写了一个demo test示例,给大家做一个参考。
|
1月前
|
程序员 Go
go语言中结构体(Struct)
go语言中结构体(Struct)
110 71
|
1月前
|
存储 Go 索引
go语言中的数组(Array)
go语言中的数组(Array)
112 67
|
2天前
|
算法 安全 Go
Go语言中的加密和解密是如何实现的?
Go语言通过标准库中的`crypto`包提供丰富的加密和解密功能,包括对称加密(如AES)、非对称加密(如RSA、ECDSA)及散列函数(如SHA256)。`encoding/base64`包则用于Base64编码与解码。开发者可根据需求选择合适的算法和密钥,使用这些包进行加密操作。示例代码展示了如何使用`crypto/aes`包实现对称加密。加密和解密操作涉及敏感数据处理,需格外注意安全性。
27 14
|
2天前
|
Go 数据库
Go语言中的包(package)是如何组织的?
在Go语言中,包是代码组织和管理的基本单元,用于集合相关函数、类型和变量,便于复用和维护。包通过目录结构、文件命名、初始化函数(`init`)及导出规则来管理命名空间和依赖关系。合理的包组织能提高代码的可读性、可维护性和可复用性,减少耦合度。例如,`stringutils`包提供字符串处理函数,主程序导入使用这些函数,使代码结构清晰易懂。
27 11
|
2天前
|
存储 安全 Go
Go语言中的map数据结构是如何实现的?
Go 语言中的 `map` 是基于哈希表实现的键值对数据结构,支持快速查找、插入和删除操作。其原理涉及哈希函数、桶(Bucket)、动态扩容和哈希冲突处理等关键机制,平均时间复杂度为 O(1)。为了确保线程安全,Go 提供了 `sync.Map` 类型,通过分段锁实现并发访问的安全性。示例代码展示了如何使用自定义结构体和切片模拟 `map` 功能,以及如何使用 `sync.Map` 进行线程安全的操作。
|
6天前
|
监控 安全 算法
深度剖析核心科技:Go 语言赋能局域网管理监控软件进阶之旅
在局域网管理监控中,跳表作为一种高效的数据结构,能显著提升流量索引和查询效率。基于Go语言的跳表实现,通过随机化索引层生成、插入和搜索功能,在高并发场景下展现卓越性能。跳表将查询时间复杂度优化至O(log n),助力实时监控异常流量,保障网络安全与稳定。示例代码展示了其在实际应用中的精妙之处。
28 9
|
16天前
|
算法 安全 Go
Go 语言中实现 RSA 加解密、签名验证算法
随着互联网的发展,安全需求日益增长。非对称加密算法RSA成为密码学中的重要代表。本文介绍如何使用Go语言和[forgoer/openssl](https://github.com/forgoer/openssl)库简化RSA加解密操作,包括秘钥生成、加解密及签名验证。该库还支持AES、DES等常用算法,安装简便,代码示例清晰易懂。
49 12