问答文章1 问答文章501 问答文章1001 问答文章1501 问答文章2001 问答文章2501 问答文章3001 问答文章3501 问答文章4001 问答文章4501 问答文章5001 问答文章5501 问答文章6001 问答文章6501 问答文章7001 问答文章7501 问答文章8001 问答文章8501 问答文章9001 问答文章9501

Go看源码必会知识之unsafe包

发布网友 发布时间:2024-09-26 15:05

我来回答

1个回答

热心网友 时间:2024-10-22 03:56

前言

有看源码的朋友应该会发现,Go标准库中大量使用了unsafe.pointer,要想更好的理解源码实现,就要知道unsafe.pointer到底是什么?所以今天就与大家来聊一聊unsafe包。

什么是unsafe

众所周知,Go语言被设计成一门强类型的静态语言,那么他的类型就不能改变了,静态也是意味着类型检查在运行前就做了。所以在Go语言中是不允许两个指针类型进行转换的,使用过C语言的朋友应该知道这在C语言中是可以实现的,Go中不允许这么使用是处于安全考虑,毕竟强制转型会引起各种各样的麻烦,有时这些麻烦很容易被察觉,有时他们却又隐藏极深,难以察觉。大多数读者可能不明白为什么类型转换是不安全的,这里用C语言举一个简单的例子:

int main(){double pi = 3.1415926;double *pv = πvoid *temp = pd;int *p = temp;}

在标准C语言中,任何非void类型的指针都可以和void类型的指针相互指派,也可以通过void类型指针作为中介,实现不同类型的指针间接相互转换。上面示例中,指针pv指向的空间本是一个双精度数据,占8个字节,但是经过转换后,p指向的是一个4字节的int类型。这种发生内存截断的设计缺陷会在转换后进行内存访问是存在安全隐患。我想这就是Go语言被设计成强类型语言的原因之一吧。

虽然类型转换是不安全的,但是在一些特殊场景下,使用了它,可以打破Go的类型和内存安全机制,可以绕过类型系统低效,提高运行效率。所以Go标准库中提供了一个unsafe包,之所以叫这个名字,就是不推荐大家使用,但是不是不能用,如果你掌握的特别好,还是可以实践的。

unsafe 实现原理

在使用之前我们先来看一下unsafe的源码部分,标准库unsafe包中只提供了3``种方法,分别是:

func Sizeof(x ArbitraryType) uintptrfunc Offsetof(x ArbitraryType) uintptrfunc Alignof(x ArbitraryType) uintptr

Sizeof(x ArbitrayType)方法主要作用是用返回类型x所占据的字节数,但并不包含x所指向的内容的大小,与C语言标准库中的Sizeof()方法功能一样,比如在32位机器上,一个指针返回大小就是4字节。

Offsetof(x ArbitraryType)方法主要作用是返回结构体成员在内存中的位置离结构体起始处(结构体的第一个字段的偏移量都是0)的字节数,即偏移量,我们在注释中看一看到其入参必须是一个结构体,其返回值是一个常量。

Alignof(x ArbitratyType)的主要作用是返回一个类型的对齐值,也可以叫做对齐系数或者对齐倍数。对齐值是一个和内存对齐有关的值,合理的内存对齐可以提高内存读写的性能。一般对齐值是2^n,最大不会超过8(受内存对齐影响).获取对齐值还可以使用反射包的函数,也就是说:unsafe.Alignof(x)等价于reflect.TypeOf(x).Align()。对于任意类型的变量x,unsafe.Alignof(x)至少为1。对于struct结构体类型的变量x,计算x每一个字段f的unsafe.Alignof(x,f),unsafe.Alignof(x)等于其中的最大值。对于array数组类型的变量x,unsafe.Alignof(x)等于构成数组的元素类型的对齐倍数。没有任何字段的空struct{}和没有任何元素的array占据的内存空间大小为0,不同大小为0的变量可能指向同一块地址。

细心的朋友会发发现这三个方法返回的都是uintptr类型,这个目的就是可以和unsafe.poniter类型相互转换,因为*T是不能计算偏移量的,也不能进行计算,但是uintptr是可以的,所以可以使用uintptr类型进行计算,这样就可以可以访问特定的内存了,达到对不同的内存读写的目的。三个方法的入参都是ArbitraryType类型,代表着任意类型的意思,同时还提供了一个Pointer指针类型,即像void *一样的通用型指针。

type ArbitraryType inttype Pointer *ArbitraryType// uintptr 是一个整数类型,它足够大,可以存储type uintptr uintptr

上面说了这么多,可能会有点懵,在这里对三种指针类型做一个总结:

*T:普通类型指针类型,用于传递对象地址,不能进行指针运算。

unsafe.poniter:通用指针类型,用于转换不同类型的指针,不能进行指针运算,不能读取内存存储的值(需转换到某一类型的普通指针)

uintptr:用于指针运算,GC不把uintptr当指针,uintptr无法持有对象。uintptr类型的目标会被回收。

三者关系就是:unsafe.Pointer是桥梁,可以让任意类型的指针实现相互转换,也可以将任意类型的指针转换为uintptr进行指针运算,也就说uintptr是用来与unsafe.Pointer打配合,用于指针运算。画个图表示一下:

基本原理就说到这里啦,接下来我们一起来看看如何使用~

unsafe.Pointer基本使用

我们在上一篇分析atomic.Value源码时,看到atomic/value.go中定义了一个ifaceWords结构,其中typ和data字段类型就是unsafe.Poniter,这里使用unsafe.Poniter类型的原因是传入的值就是interface{}类型,使用unsafe.Pointer强转成ifaceWords类型,这样可以把类型和值都保存了下来,方便后面的写入类型检查。截取部分代码如下:

// ifaceWords is interface{} internal representation.type ifaceWords struct { typunsafe.Pointer data unsafe.Pointer}// Load returns the value set by the most recent Store.// It returns nil if there has been no call to Store for this Value.func (v *Value) Load() (x interface{}) { vp := (*ifaceWords)(unsafe.Pointer(v))for {typ := LoadPointer(&vp.typ) // 读取已经存在值的类型/**..... 中间省略**/// First store completed. Check type and overwrite data.if typ != xp.typ { //当前类型与要存入的类型做对比 panic("sync/atomic: store of inconsistently typed value into Value")}}

上面就是源码中使用unsafe.Pointer的一个例子,有一天当你准备读源码时,unsafe.pointer的使用到处可见。好啦,接下来我们写一个简单的例子,看看unsafe.Pointer是如何使用的。

func main(){ number := 5 pointer := &number fmt.Printf("number:addr:%p, value:%d\n",pointer,*pointer) float32Number := (*float32)(unsafe.Pointer(pointer)) *float32Number = *float32Number + 3 fmt.Printf("float64:addr:%p, value:%f\n",float32Number,*float32Number)}

运行结果:

number:addr:0xc000018090, value:5float64:addr:0xc000018090, value:3.000000

由运行可知使用unsafe.Pointer强制类型转换后指针指向的地址是没有改变,只是类型发生了改变。这个例子本身没什么意义,正常项目中也不会这样使用。

总结一下基本使用:先把*T类型转换成unsafe.Pointer类型,然后在进行强制转换转成你需要的指针类型即可。

Sizeof、Alignof、Offsetof三个函数的基本使用

先看一个例子:

type User struct { Name string Age uint32 Gender bool // 男:true 女:false 就是举个例子别吐槽我这么用。。。。}func func_example(){ // sizeof fmt.Println(unsafe.Sizeof(true)) fmt.Println(unsafe.Sizeof(int8(0))) fmt.Println(unsafe.Sizeof(int16(10))) fmt.Println(unsafe.Sizeof(int(10))) fmt.Println(unsafe.Sizeof(int32(190))) fmt.Println(unsafe.Sizeof("asong")) fmt.Println(unsafe.Sizeof([]int{1,3,4})) // Offsetof user := User{Name: "Asong", Age: 23,Gender: true} userNamePointer := unsafe.Pointer(&user) nNamePointer := (*string)(unsafe.Pointer(userNamePointer)) *nNamePointer = "Golang梦工厂" nAgePointer := (*uint32)(unsafe.Pointer(uintptr(userNamePointer) + unsafe.Offsetof(user.Age))) *nAgePointer = 25 nGender := (*bool)(unsafe.Pointer(uintptr(userNamePointer)+unsafe.Offsetof(user.Gender))) *nGender = false fmt.Printf("u.Name: %s, u.Age: %d,u.Gender: %v\n", user.Name, user.Age,user.Gender) // Alignof 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))}

为了省事,把三个函数的使用示例放到了一起,首先看sizeof方法,我们可以知道各个类型所占字节大小,这里重点说一下int类型,Go语言中的int类型的具体大小是跟机器的 CPU位数相关的。如果 CPU 是32 位的,那么int就占4字节,如果 CPU是64位的,那么 int 就占8 字节,这里我的电脑是64位的,所以结果就是8字节。

然后我们在看Offsetof函数,我想要修改结构体中成员变量,第一个成员变量是不需要进行偏移量计算的,直接取出指针后转换为unsafe.pointer,在强制给他转换成字符串类型的指针值即可。如果要修改其他成员变量,需要进行偏移量计算,才可以对其内存地址修改,所以Offsetof方法就可返回成员变量在结构体中的偏移量,也就是返回结构体初始位置到成员变量之间的字节数。看代码时大家应该要住uintptr的使用,不可以用一个临时变量存储uintptr类型,前面我们提到过用于指针运算,GC不把uintptr当指针,uintptr无法持有对象。uintptr类型的目标会被回收,所以你不知道他什么时候会被GC掉,那样接下来的内存操作会发生什么样的错误,咱也不知道。比如这样一个例子:

// 切记不要这样使用p1 := uintptr(userNamePointer)nAgePointer := (*uint32)(unsafe.Pointer(p1 + unsafe.Offsetof(user.Age)))

最后看一下Alignof函数,主要是获取变量的对齐值,除了int、uintptr这些依赖CPU位数的类型,基本类型的对齐值都是固定的,结构体中对齐值取他的成员对齐值的最大值,结构体的对齐涉及到内存对齐,我们在下面详细介绍。

经典应用:string与[]byte的相互转换

实现string与byte的转换,正常情况下,我们可能会写出这样的标准转换:

// string to []bytestr1 := "Golang梦工厂"by := []byte(s1)// []byte to stringstr2 := string(by)

使用这种方式进行转换都会涉及底层数值的拷贝,所以想要实现零拷贝,我们可以使用unsafe.Pointer来实现,通过强转换直接完成指针的指向,从而使string和[]byte指向同一个底层数据。在reflect包中有·string和slice对应的结构体,他们的分别是:

type StringHeader struct { Data uintptr Lenint}type SliceHeader struct { Data uintptr Lenint Capint}

StringHeader代表的是string运行时的表现形式(SliceHeader同理),通过对比string和slice运行时的表达可以看出,他们只有一个Cap字段不同,所以他们的内存布局是对齐的,所以可以通过unsafe.Pointer进行转换,因为可以写出如下代码:

func Sizeof(x ArbitraryType) uintptrfunc Offsetof(x ArbitraryType) uintptrfunc Alignof(x ArbitraryType) uintptr0

上面的代码我们通过重新构造slice header和string header完成了类型转换,其实[]byte转换成string可以省略掉自己构造StringHeader的方式,直接使用强转就可以,因为string的底层也是[]byte,强转会自动构造,省略后的代码如下:

func Sizeof(x ArbitraryType) uintptrfunc Offsetof(x ArbitraryType) uintptrfunc Alignof(x ArbitraryType) uintptr1

虽然这种方式更高效率,但是不推荐大家使用,前面也提高到了,这要是不安全的,使用当不当会出现极大的隐患,一些严重的情况recover也不能捕获。

内存对齐

现在计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但是实际情况是在访问特定类型变量的时候经常在特定的内存地址访问,这就需要各种类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就对齐。

对齐的作用和原因:CPU访问内存时,并不是逐个字节访问,而是以字长(word size)单位访问。比如32位的CPU,字长为4字节,那么CPU访问内存的单位也是4字节。这样设计可以减少CPU访问内存的次数,加大CPU访问内存的吞吐量。假设我们需要读取8个字节的数据,一次读取4个字节那么就只需读取2次就可以。内存对齐对实现变量的原子性操作也是有好处的,每次内存访问都是原子的,如果变量的大小不超过字长,那么内存对齐后,对该变量的访问就是原子的,这个特性在并发场景下至关重要。

我们来看这样一个例子:

func Sizeof(x ArbitraryType) uintptrfunc Offsetof(x ArbitraryType) uintptrfunc Alignof(x ArbitraryType) uintptr2

从结果可以看出,字段放置不同的顺序,占用内存也不一样,这就是因为内存对齐影响了struct的大小,所以有时候合理的字段可以减少内存的开销。下面我们就一起来分析一下内存对齐,首先要明白什么是内存对齐的规则,C语言的对齐规则与Go语言一样,所以C语言的对齐规则对Go同样适用:

对于结构的各个成员,第一个成员位于偏移为0的位置,结构体第一个成员的偏移量(offset)为0,以后每个成员相对于结构体首地址的 offset 都是该成员大小与有效对齐值中较小那个的整数倍,如有需要编译器会在成员之间加上填充字节。

除了结构成员需要对齐,结构本身也需要对齐,结构的长度必须是编译器默认的对齐长度和成员中最长类型中最小的数据大小的倍数对齐。

好啦,知道规则了,我们现在来分析一下上面的例子,根据我的mac使用的64位CPU,对齐参数是8来分析,int32、[]int32、string、bool对齐值分别是4、8、8、1,占用内存大小分别是4、24、16、1,我们先根据第一条对齐规则分析User1:

第一个字段类型是int32,对齐值是4,大小为4,所以放在内存布局中的第一位.

第二个字段类型是[]int32,对齐值是8,大小为24,所以他的内存偏移值必须是8的倍数,所以在当前user1中,就不能从第4位开始了,必须从第5位开始,也就偏移量为8。第4,5,6,7位由编译器进行填充,一般为0值,也称之为空洞。第9位到第32位为第二个字段B.

第三个字段类型是string,对齐值是8,大小为16,所以他的内存偏移值必须是8的倍数,因为user1前两个字段就已经排到了第32位,所以下一位的偏移量正好是32,正好是字段C的对齐值的倍数,不用填充,可以直接排列第三个字段,也就是从第32位到48位第三个字段C.

第三个字段类型是bool,对齐值是1,大小为1,所以他的内存偏移值必须是1的倍数,因为user1前两个字段就已经排到了第48位,所以下一位的偏移量正好是48。正好是字段D的对齐值的倍数,不用填充,可以直接排列到第四个字段,也就是从48到第49位是第三个字段D.

好了现在第一条内存对齐规则后,内存长度已经为49字节,我们开始使用内存的第2条规则进行对齐。根据第二条规则,默认对齐值是8,字段中最大类型程度是24,取最小的那一个,所以求出结构体的对齐值是8,我们目前的内存长度是49,不是8的倍数,所以需要补齐,所以最终的结果就是56,补了7位。

说了这么多,画个图看一下吧:

现在你们应该懂了吧,按照这个思路再去分析其他两个struct吧,这里就不再分析了。

对于内存对齐这里还有一最后需要注意的知识点,空struct不占用任何存储空间,空 struct{} 大小为 0,作为其他 struct 的字段时,一般不需要内存对齐。但是有一种情况除外:即当 struct{} 作为结构体最后一个字段时,需要内存对齐。因为如果有指针指向该字段, 返回的地址将在结构体之外,如果此指针一直存活不释放对应的内存,就会有内存泄露的问题(该内存不因结构体释放而释放)。来看一个例子:

func Sizeof(x ArbitraryType) uintptrfunc Offsetof(x ArbitraryType) uintptrfunc Alignof(x ArbitraryType) uintptr3

简单来说,对于任何占用0大小空间的类型,像struct {}或者[0]byte这些,如果该类型出现在结构体末尾,那么我们就假设它占用1个字节的大小。因此对于test1结构体,他看起来就是这样:`

func Sizeof(x ArbitraryType) uintptrfunc Offsetof(x ArbitraryType) uintptrfunc Alignof(x ArbitraryType) uintptr4

因此在内存对齐时,最后结构体占用的字节就是8了。

重点要注意的问题:不要在结构体定义的最后添加零大小的类型

总结

好啦,终于又到文章的末尾了,我们来简单的总结一下,unsafe 包绕过了 Go 的类型系统,达到直接操作内存的目的,使用它有一定的风险性。但是在某些场景下,使用 unsafe 包提供的函数会提升代码的效率,Go 源码中也是大量使用 unsafe 包。

unsafe 包定义了 Pointer 和三个函数:

type ArbitraryType inttype Pointer *ArbitraryTypefunc Sizeof(x ArbitraryType) uintptrfunc Offsetof(x ArbitraryType) uintptrfunc Alignof(x ArbitraryType) uintptr

uintptr 可以和 unsafe.Pointer 进行相互转换,uintptr 可以进行数*算。这样,通过 uintptr 和 unsafe.Pointer 的结合就解决了 Go 指针不能进行数*算的*。通过 unsafe 相关函数,可以获取结构体私有成员的地址,进而对其做进一步的读写操作,突破 Go 的类型安全*。

最后我们又学习了内存对齐的知识,这样设计可以减少CPU访问内存的次数,加大CPU访问内存的吞吐量,所以结构体中字段合理的排序可以更节省内存,注意:不要在结构体定义的最后添加零大小的类型。

原文:https://juejin.cn/post/7099353249613873166

好啦,这篇文章就到这里啦,素质三连(分享、点赞、在看)都是笔者持续创作更多优质内容的动力!

创建了一个Golang学习交流群,欢迎各位大佬们踊跃入群,我们一起学习交流。入群方式:加我vx拉你入群,或者公众号获取入群二维码

结尾给大家发一个小福利吧,最近我在看[微服务架构设计模式]这一本书,讲的很好,自己也收集了一本PDF,有需要的小伙可以到自行下载。获取方式:关注公众号:[Golang梦工厂],后台回复:[微服务],即可获取。

我翻译了一份GIN中文文档,会定期进行维护,有需要的小伙伴后台回复[gin

声明声明:本网页内容为用户发布,旨在传播知识,不代表本网认同其观点,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。E-MAIL:11247931@qq.com
小鹿的过冬方式是什么 我弟弟生气,拍了下电脑桌,结果再开机就说电源按钮关闭,显示器休眠 为什么进入屏保后几分钟显示器又亮了起来 我的手碰电脑桌显示器经常闪一下 下一站江湖所有隐藏功法 最新隐藏功法级别 《下一站江湖》玄龟软甲获得方法介绍_《下一站江湖》玄龟软甲获得方法是... 如何选购前锋热水器 近年美国校园电影,青春喜剧 最好是08-11年的,新的。不要悲剧的。 推荐几部美国校园喜剧电影,谢谢! 美国 八九十年代 的 电视剧 电影 讲美国 八十年代的也可以 内容要有摇... 企业辞退怀孕员工的补偿标准2024年 笔记本开机一直嗡嗡响,怎么回事? 选择理论2——消费者问题:偏好假设,特殊案例,Marshallian Demand... 无差异曲线取决于什么 女孩有意境名字有文化2023 胃癌早期的症状是什么 胃部肿瘤的症状 qq空间怎么创建模块【高分】详细点的 胃肿瘤的早期症状有哪些 如何制作自己喜欢的QQ空间大图模块 如何在QQ空间中添加图文模块并进行自定义排布? OO空间自定义模块怎么弄?图片的 详细 QQ空间如何使用大图素材要详细的步骤 如何制作QQ空间套装模块? 如何在QQ空间中添加图形模块并设置图片地址? 坐飞机哪些东西不能托运 1996年阴历10月14日白天13点50分出生的男孩是什么命,取什么名字好一些... 早期结直肠癌要化疗吗 直肠癌患者的生存率 Ⅰ期直肠癌可以根治吗 ...直接改变对应实参的值时,该形式参数应说明为什么参数? 成都辞退孕妇补偿标准2024是什么 2013款奔驰GL350国内有现车吗?报价是多少? ...指向函数的指针有什么特殊的作用吗?为什么要有指向函数的指针呢... 奔驰gl350邮箱多少升? 我有张邮政淘宝联合卡,存钱进去是不是一定要到柜台存才行 如何激活已办理的邮政淘宝联名卡? 武汉一新生为何一人睡两张床? 办了邮政绿卡(淘宝联名卡).接着要怎么激活? ...李子核中为什么大家笑的时候,万尼亚却哭了? 每天精神极度紧张会不会得病 在武汉科技大学城市学院读书的请来看看 请问办理了绿卡(淘宝联名卡)后还要另外办理支付宝业务吗?怎么办理啊... 《实况足球2013》一球成名内存修改方法介绍_《实况足球2013》一球成名... 李子核全文 武汉科技大学黄家湖校区女生寝室怎样啊 全是四人间么 有图么 学校可以... 高考前极度紧张如何有效缓解? 已有淘宝联名卡怎样支付宝实名认证 关于邮政绿卡(淘宝联名卡)的问题。 淘宝绿卡联名卡跟支付宝账号捆绑一定要到柜台前办理吗