其他分享
首页 > 其他分享> > 深度解密Go语言之unsafe

深度解密Go语言之unsafe

作者:互联网

文章目录

指针类型

在正式介绍 unsafe 包之前,需要着重介绍 Go 语言中的指针类型。

我本科开始学编程的时候,第一门语言就是 C。之后又陆续学过 C++,Java,Python,这些语言都挺强大的,但是没了 C 语言那么“单纯”。直到我开始接触 Go 语言,又找到了那种感觉。Go 语言的作者之一 Ken Thompson 也是 C 语言的作者。所以,Go 可以看作 C 系语言,它的很多特性都和 C 类似,指针就是其中之一。

然而,Go 语言的指针相比 C 的指针有很多限制。这当然是为了安全考虑,要知道像 Java/Python 这些现代语言,生怕程序员出错,哪有什么指针(这里指的是显式的指针)?更别说像 C/C++ 还需要程序员自己清理“垃圾”。所以对于 Go 来说,有指针已经很不错了,仅管它有很多限制。

为什么需要指针类型呢?参考文献 go101.org 里举了这样一个例子:

package main

import "fmt"

func double(x int) {
	x += x
}
func main() {
	var a = 3
	double(a)
	fmt.Println(a) //3
}

非常简单,我想在 double 函数里将 a 翻倍,但是例子中的函数却做不到。为什么?因为 Go 语言的函数传参都是 值传递。double 函数里的 x 只是实参 a 的一个拷贝,在函数内部对 x 的操作不能反馈到实参 a。

如果这时,有一个指针就可以解决问题了!这也是我们常用的“伎俩”。

package main

import "fmt"

func double(x *int) {
	*x += *x
	x = nil
}
func main() {
	var a = 3
	double(&a)
	fmt.Println(a) //6

	p := &a
	double(p)
	fmt.Println(a, p == nil) //12 false
}

很常规的操作,不用多解释。唯一可能有些疑惑的在这一句:

x =  nil

这得稍微思考一下,才能得出这一行代码根本不影响的结论。因为是值传递,所以 x 也只是对 &a 的一个拷贝。

*x += *x

这一句把 x 指向的值(也就是 &a 指向的值,即变量 a)变为原来的 2 倍。但是对 x 本身(一个指针)的操作却不会影响外层的 a,所以 x=nil 掀不起任何大风大浪。

下面的这张图可以“自证清白”:
在这里插入图片描述
然而,相比于 C 语言中指针的灵活,Go 的指针多了一些限制。但这也算是 Go 的成功之处:既可以享受指针带来的便利,又避免了指针的危险性。

限制一:Go的指针不能进行数学运算。

	a := 5
	p := &a
	p++
	p = &a + 3

上面的代码将不能通过编译,会报编译错误:invalid operation,也就是说不能对指针做数学运算。

限制二:不同类型的指针不能相互转换。

例如下面这个简短的例子:
来看一个简单的例子:

func main() {
	a := int(100)
	var f *float64
	f = &a
}

也会报编译错误:cannot use &a (type *int) as type *float64 in assignment

关于两个指针能否相互转换,参考资料中 go 101 相关文章里写得非常细,这里我不想展开。个人认为记住这些没有什么意义,有完美主义的同学可以去阅读原文。当然我也有完美主义,但我有时会克制,嘿嘿。

限制三:不同类型的指针不能使用==或!=比较。

只有在两个指针类型相同或者可以相互转换的情况下,才可以对两者进行比较。另外,指针可以通过 == 和 != 直接和 nil 作比较。

限制四:不同类型的指针变量不能相互赋值。

这一点同限制三。

什么是unsafe

前面所说的指针是类型安全的,但它有很多限制。Go 还有非类型安全的指针,这就是 unsafe 包提供的 unsafe.Pointer。在某些情况下,它会使代码更高效,当然,也更危险。

unsafe 包用于 Go 编译器,在编译阶段使用。从名字就可以看出来,它是不安全的,官方并不建议使用。我在用 unsafe 包的时候会有一种不舒服的感觉,可能这也是语言设计者的意图吧。

但是高阶的 Gopher,怎么能不会使用 unsafe 包呢?它可以绕过 Go 语言的类型系统,直接操作内存。例如,一般我们不能操作一个结构体的未导出成员,但是通过 unsafe 包就能做到。unsafe 包让我可以直接读写内存,还管你什么导出还是未导出。

为什么有unsafe

Go 语言类型系统是为了安全和效率设计的,有时,安全会导致效率低下。有了 unsafe 包,高阶的程序员就可以利用它绕过类型系统的低效。因此,它就有了存在的意义,阅读 Go 源码,会发现有大量使用 unsafe 包的例子。

unsafe实现原理

我们来看源码:

type ArbitraryType int
type Pointer *ArbitraryType

从命名来看, Arbitrary 是任意的意思,也就是说 Pointer 可以指向任意类型,实际上它类似于 C 语言里的 void*。

unsafe 包还有其他三个函数:

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

Sizeof 返回类型 x 所占据的字节数,但不包含 x 所指向的内容的大小。例如,对于一个指针,函数返回的大小为 8 字节(64位机上),一个 slice 的大小则为 slice header 的大小。

Offsetof 返回结构体成员在内存中的位置离结构体起始处的字节数,所传参数必须是结构体的成员。

Alignof 返回 m,m 是指当类型进行内存对齐时,它分配到的内存地址能整除 m。

注意到以上三个函数返回的结果都是 uintptr 类型,这和 unsafe.Pointer 可以相互转换。三个函数都是在编译期间执行,它们的结果可以直接赋给 const型变量。另外,因为三个函数执行的结果和操作系统、编译器相关,所以是不可移植的。

综上所述,unsafe 包提供了 2 点重要的能力:

  1. 任何类型的指针和 unsafe.Pointer 可以相互转换。
  2. uintptr 类型和 unsafe.Pointer 可以相互转换。
    在这里插入图片描述
    pointer 不能直接进行数学运算,但可以把它转换成 uintptr,对 uintptr 类型进行数学运算,再转换成 pointer 类型。
// uintptr 是一个整数类型,它足够大,可以存储
type uintptr uintptr

还有一点要注意的是,uintptr 并没有指针的语义,意思就是 uintptr 所指向的对象会被 gc 无情地回收。而 unsafe.Pointer 有指针语义,可以保护它所指向的对象在“有用”的时候不会被垃圾回收。

unsafe 包中的几个函数都是在编译期间执行完毕,毕竟,编译器对内存分配这些操作“了然于胸”。在 /usr/local/go/src/cmd/compile/internal/gc/unsafe.go 路径下,可以看到编译期间 Go 对 unsafe 包中函数的处理。

更深层的原理需要去研究编译器的源码,这里就不去深究了。我们重点关注它的用法,接着往下看。

unsafe如何使用

总结

unsafe 包绕过了 Go 的类型系统,达到直接操作内存的目的,使用它有一定的风险性。但是在某些场景下,使用 unsafe 包提供的函数会提升代码的效率,Go 源码中也是大量使用 unsafe 包。

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

type ArbitraryType int
type Pointer *ArbitraryType

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

通过三个函数可以获取变量的大小、偏移、对齐等信息。

uintptr 可以和 unsafe.Pointer 进行相互转换,uintptr 可以进行数学运算。这样,通过 uintptr 和 unsafe.Pointer 的结合就解决了 Go 指针不能进行数学运算的限制。

通过 unsafe 相关函数,可以获取结构体私有成员的地址,进而对其做进一步的读写操作,突破 Go 的类型安全限制。关于 unsafe 包,我们更多关注它的用法。

顺便说一句,unsafe 包用多了之后,也不觉得它的名字有多么地不“美观”了。相反,因为使用了官方并不提倡的东西,反而觉得有点酷炫。这就是叛逆的感觉吧。

参考资料

【原作者地址】https://mp.weixin.qq.com/s/uTlzmsEg5OtbzObMbt3p0w
【飞雪无情的博客】https://www.flysnow.org/2017/07/06/go-in-action-unsafe-pointer.html

【译文 unsafe包详解】https://gocn.vip/question/371

【官方文档】https://golang.org/pkg/unsafe/

【例子】http://www.opscoder.info/golang_unsafe.html

【煎鱼大佬的博客】https://segmentfault.com/a/1190000017389782

【go语言圣经】https://www.kancloud.cn/wizardforcel/gopl-zh/106477

【pointer and system calls】https://blog.gopheracademy.com/advent-2017/unsafe-pointer-and-system-calls/

【pointer and uintptr】https://my.oschina.net/xinxingegeya/blog/729673

【unsafe.pointer】https://go101.org/article/unsafe.html

【go 指针类型】https://go101.org/article/pointer.html

【码洞 快学Go语言 unsafe】https://juejin.im/post/5c189dce5188256b2e71e79b

【官方文档】https://golang.org/pkg/unsafe/

【jasper 的小窝】http://www.opscoder.info/golang_unsafe.html

标签:int,uintptr,unsafe,解密,Go,Pointer,指针
来源: https://blog.csdn.net/mingtiannihaoabc/article/details/120787561