当前位置: 首页 > 科技观察

Go团队不推荐Unsafe.Pointer

时间:2023-03-17 13:06:40 科技观察

的详解本文转载请联系脑筋急转弯公众号。大家好,我是炸鱼。大家在学习Go的时候一定学过“Go的指针不支持指针运算和转换”这个知识点。为什么?首先,Go是静态语言,所有的变量都必须是标量类型。不同的类型不能进行赋值、计算等跨类型的操作。那么指针也对应了相对类型,这也在Compile的静态类型检查范围之内。同时静态语言也称为强类型。也就是说,一旦定义,就无法更改。错误示例funcmain(){num:=5numPointer:=&numflnum:=(*float32)(numPointer)fmt.Println(flnum)}输出结果:#command-line-arguments...:cannotconvertnumPointer(type*int)totype*float32例子中,我们创建了一个数值为5,类型为int的num变量,准备干点大事。接下来,我们取了指针的地址,试图将其强制转换为*float32,结果失败了……万能的破墙unsafe针对的是刚才的“错误示例”,我们可以使用今天的英雄unsafe标准库来解决它。这是一个了不起的包裹。在官方的解释中,概括如下:围绕内存安全的操作和围棋程序的类型。它很可能是不可携带的。未包含在Go1兼容性指南中。简单来说就是不建议大家使用,因为不安全(unsafe)。但在特殊场景下,使用它可以打破Go的类型和内存安全机制,让你获得意想不到的效果。unsafe.Pointer为了解决这个问题,需要使用unsafe.Pointer。它表示任意类型的可寻址指针值,可以在不同的指针类型之间进行转换(类似于C语言中void*的用法)。它包含四个核心操作:任何类型的指针值都可以转换为Pointer。指针可以转换为任何类型的指针值。uintptr可以转换为指针。指针可以转换为uintptr。在这一部分中,重点关注第一点和第二点。再想想怎么修改“错误的例子”让它跑起来?修改如下:funcmain(){num:=5numPointer:=&numflnum:=(*float32)(unsafe.Pointer(numPointer))fmt.Println(flnum)}Outputresult:0xc4200140b0在上面的代码中,我们做了一些改动.通过unsafe.Pointer的特性修改指针变量,可以完成任意类型(*T)的指针转换。需要注意的是,此时不能对变量进行操作和访问,因为指针地址指向的对象具体类型是未知的。如果你不知道它是什么类型,你怎么能解析它呢?如果你不能解析它,你就不能改变它。unsafe.Offsetof上一节我们修改了普通指针变量。那么它可以做更复杂的事情吗?typeNumstruct{istringjint64}funcmain(){n:=Num{i:"EDDYCJY",j:1}nPointer:=unsafe.Pointer(&n)niPointer:=(*string)(unsafe.Pointer(nPointer))*niPointer="炸鱼"njPointer:=(*int64)(unsafe.Pointer(uintptr(nPointer)+unsafe.Offsetof(n.j)))*njPointer=2fmt.Printf("n.i:%s,n.j:%d",n.i,n.j)}输出结果:n.i:friedfish,n.j:2在分析这段代码做什么之前,我们需要了解结构体的一些基本概念:结构体成员变量是内存存储上的一块连续内存。结构的初始地址是第一个成员变量的内存地址。根据结构成员的地址计算偏移量。可以得到其他成员变量的内存地址。回过头来看上面的代码,得到执行流程:修改n.i的值:i为第一个成员变量。所以不需要计算偏移量,直接把指针取出来转成Pointer,再强制转换成string类型的指针值即可。修改n.j值:j为第二个成员变量。在修改内存地址之前需要进行偏移量计算。经过偏移操作,当前地址已经指向第二个成员变量。然后重复转换赋值。详细分析需要注意,这里使用了以下方法(完成偏移量计算的目标):1.uintptr:uintptr是Go的内置类型。返回一个可以存储完整地址的无符号整数。后续常用的指针操作typeuintptruintptr2,unsafe.Offsetof:返回成员变量x在结构体中的偏移量。更具体地说,它返回结构的初始位置和x之间的字节数。需要注意的是入参ArbitraryType代表的是任意类型,不是定义的int。它的实际作用是这部分中的一个占位符funcOffsetof(xArbitraryType)uintptr,其实它巧妙地利用了Pointer的第三个和第四个特性。这时候,已经可以对变量进行操作了。坏例子funcmain(){n:=Num{i:"EDDYCJY",j:1}nPointer:=unsafe.Pointer(&n)...ptr:=uintptr(nPointer)njPointer:=(*int64)(unsafe.Pointer(ptr+unsafe.Offsetof(n.j)))...}这里有个问题,uintptr类型不能存放在临时变量中。因为从GC的角度来看,uintptr类型的临时变量只是一个无符号整数,并不知道它是一个指针地址。因此,当满足某些条件时,临时变量ptr可能会被垃圾回收,那么接下来的内存操作是不是很玄乎?两个知识点的简单总结和回顾如下:第一个是unsafe.Pointer可以让你的Variables在不同的指针类型之间来回传递,也就是表示为任意可寻址的指针类型。二是经常使用uintptr配合unsafe.Pointer进行指针运算,非常巧妙。最后一句是没有特殊需要。不建议使用不安全的标准库,不安全。虽然它常常能让你眼前一亮。