浅析 unsafe.Pointer 与 uintptr

点击上方蓝色“Golang来啦”关注我哟

加个“星标”,天天 15 分钟,掌握 Go 语言

大家好,我是四哥。

看过 Go 相关源码的同学,应该会注意到不少地方使用了 unsafe.Pointer 和 uintptr,单从类型名称看,这些与“指针”是不是有什么关系?

先附上一张关系图,后面我们再展开解析。

4d2ed5080219efad2b5ab56d092b47f9.png

普通指针类型

我们一般将 *T 看作指针类型,表示一个指向 T 类型变量的指针。我们都知道 Go 是强类型语言,声明变量之后,变量的类型是不可以改变的,不同类型的指针也不允许相互转化。例如下面这样:

func main(){
 i := 30
 iPtr1 := &i

 var iPtr2 *int64 = (*int64)(iPtr1)

 fmt.Println(iPtr2)
}

编译报错:cannot convert iPtr1 (type *int) to type *int64,提示不能进行强制转化。

那怎么办,如何实现相互转化?

还好 Go 官方提供了 unsafe 包,有相关的解决方案。

unsafe.Pointer

unsafe.Pointer 通用指针类型,一种特殊类型的指针,可以包含任意类型的地址,能实现不同的指针类型之间进行转换,类似于 C 语言里的 void* 指针。

type ArbitraryType int

type Pointer *ArbitraryType

从定义可以看出,Pointer 实际上是 *int。

官方文档里还描述了 Pointer 的四种操作规则:

  1. 任何类型的指针都可以转化成 unsafe.Pointer;

  2. unsafe.Pointer 可以转化成任何类型的指针;

  3. uintptr 可以转换为 unsafe.Pointer;

  4. unsafeP.ointer 可以转换为 uintptr;

不同类型的指针允许相互转化实际上是运用了第 1、2 条规则,我们就着例子看下:

func main(){
 i := 30
 iPtr1 := &i

 var iPtr2 *int64 = (*int64)(unsafe.Pointer(iPtr1))

 *iPtr2 = 8

 fmt.Println(i)
}

输出:

8

上面的代码,我们可以把 *int 转为 *int64,并且对新的 *int64 进行操作,从输出会发现 i 的值被改变了。

可以说 unsafe.Pointer 是桥梁,可以让任意类型的指针实现相互转换。

我们知道 Go 语言是不支持指针运算,想要实现该怎么办?

看看第 3、4 条规则,uintptr 就可以派上用场了。

uintptr

源码定义:

// uintptr is an integer type that is large enough to hold the bit pattern of
// any pointer.
type uintptr uintptr

uintptr 是 Go 内置类型,表示无符号整数,可存储一个完整的地址。常用于指针运算,只需将 unsafe.Pointer 类型转换成 uintptr 类型,做完加减法后,转换成 unsafe.Pointer,通过 * 操作,取值或者修改值都可以。

下面是一个通过指针偏移修改结构体成员的例子,演示下 uintptr 的用法:

type Admin struct {
 Name string
 Age int
}

func main(){
 admin := Admin{
  Name: "seekload",
  Age: 18,
 }
 ptr := &admin
 name := (*string)(unsafe.Pointer(ptr))   // 1

 *name = "四哥"

 fmt.Println(*ptr)

 age := (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(ptr)) + unsafe.Offsetof(ptr.Age)))  // 2
 *age = 35

 fmt.Println(*ptr)
}

输出:

{四哥 18}
{四哥 35}

特别提下,unsafe.Offsetof 的作用是返回成员变量 x 在结构体当中的偏移量,即返回结构体初始内存地址到 x 之间的字节数。

//1 因为结构体初始地址就是第一个成员的地址,又 Name 是结构体第一个成员变量,所以此处不用偏移,我们拿到 admin 的地址,然后通过 unsafe.Pointer 转为 *string,再进行赋值操作即可。

//2 成员变量 Age 不是第一个字段,想要修改它的值就需要内存偏移。我们先将 admin 的指针转化为 uintptr,再通过 unsafe.Offsetof() 获取到 Age 的偏移量,两者都是 uintptr,进行相加指针运算获取到成员 Age 的地址,最后需要将 uintptr 转化为 unsafe.Pointer,再转化为 *int,才能对 Age 操作。

总结

这篇文章我们简单介绍了普通指针类型、unsafe.Pointer 和 uintptr 之间的关系(见文章开头关系图),记住两点:

  1. unsafe.Pointer 可以实现不同类型指针之间相互转化;

  2. uintptr 搭配着 unsafe.Pointer 使用,实现指针运算;

不过,官方不推荐使用 unsafe 包,正如它的命名一样,是不安全的,比如涉及到内存操作,这是绕过 Go 本身设计的安全机制的,不当的操作,可能会破坏一块内存,而且这种问题非常不好定位。

推荐阅读:

面试官:说说unsafe.Pointer和uintptr的区别和联系

资料下载

点击下方卡片关注公众号,发送特定关键字获取对应精品资料!

  • 回复「电子书」,获取入门、进阶 Go 语言必看书籍。

  • 回复「视频」,获取价值 5000 大洋的视频资料,内含实战项目(不外传)!

  • 回复「路线」,获取最新版 Go 知识图谱及学习、成长路线图。

  • 回复「面试题」,获取四哥精编的 Go 语言面试题,含解析。

  • 回复「后台」,获取后台开发必看 10 本书籍。

对了,看完文章,记得点击下方的卡片。关注我哦~ 👇👇👇

如果您的朋友也在学习 Go 语言,相信这篇文章对 TA 有帮助,欢迎转发分享给 TA,非常感谢!eae6e05860a7ea1c3f3d2a517eddb049.png

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Seekload

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值