如果你经常看 Go 的源码,一定会遇到 unsafe.Pointer 类型,例如上篇文章 map 源码解析中有这样一段代码:
// 获取下一个溢出桶
func (b *bmap) overflow(t *maptype) *bmap {
return *(**bmap)(add(unsafe.Pointer(b), uintptr(t.bucketsize)-sys.PtrSize))
}
上边这段代码利用 unsafe.Pointer
和 uintptr
类型的特性,通过直接对内存地址进行操作的方式,实现了链表下一节点的查找,使得代码更加高效。当然有利自然有弊,它实际上绕开了 Go 语言的类型安全机制,通过它可以直接操作内存,这种行为无疑是危险的,就如 unsafe 包的命名一样,官方都认为这个类型是很不安全的,所以轻易不要使用,但我们要解它,才能更好的阅读源码。
因为 unsafe.Pointer
与指针息息相关,因此我们先简单来了解一下 Go 语言中的指针。
变量的本质是对一块内存空间的命名,我们可以通过引用变量名来使用这块内存空间存储的值,而指针则是用来指向这些变量值所在内存地址的值。
Go 语言支持指针,如果一个变量是指针类型的,那么就可以用这个变量来存储指针类型的值。简单举个例子:
package main
import "fmt"
func main() {
var x *int
a := 1
x = &a
fmt.Println("a:", a, ",", "x:", x)
a++
fmt.Println("&a:", &a, ",", "*x:", *x)
*x++
fmt.Println("a:", a, ",", "*x:", *x)
x = nil
fmt.Println("a:", a, ",", "x:", x)
}
// 输出结果
a: 1 , x: 0xc000120000
&a: 0xc000120000 , *x: 2
a: 3 , *x: 3
a: 3 , x: <nil>
我们解释一下上边的代码,了解一下指针的基本使用:
&a
表示取变量 a
的地址,也就是 0xc000120000
;
x
变量是指针变量,指向了变量 a
所在的内存地址 0xc000120000
;
*x
表示解引用得到内存地址存储的变量 a
,此时 *x
等价于 a
,因此变量 a
的修改会影响 *x
,相反 *x
的修改也会反应到变量 a
上;但 x = nil
改变了指针变量 x
的指向,只是断开了与变量 a
的引用关系,因此并不能影响变量 a
。
指针变量常被用作参数传递,不仅可以节省内存空间,还可以在调用函数中实现对变量值的修改。 举个例子对比一下 Go 语言中的参数传递:
package main
import "fmt"
func swap1(a, b int) {
a, b = b, a
}
func swap2(a, b *int) {
a, b = b, a
}
func swap3(a, b *int) {
*a, *b = *b, *a
}
func main() {
a := 1
b := 2
swap1(a, b)
fmt.Println(a, b)
swap2(&a, &b)
fmt.Println(a, b)
swap3(&a, &b)
fmt.Println(a, b)
}
我们都知道 Go 中函数参数为“值传递”,也就意味着传递到函数 swap 的都是 a,b 变量的副本:
因此,只有通过指针变量修改了所指向的内存地址中存储的值时,才会对原变量产生影响;如果只是对变量的副本做改变,是不会影响原变量的。
Go 既然有指针了,为什么还需要 unsafe.Pointer 类型呢?
这就得聊一聊 Go 语言中对指针的一些限制了:
&a++
var a int = 1;f := (*float64)(&a)
var a int = 1;var f float64;f = &a;&a == &f
以上指针错误的使用方式在 Go 中都会编译报错。
Go 语言是一门强类型的静态语言,意味着类型一旦定义就不能改变,为了安全考虑,对指针做了以上限制。然而,为了性能的高效,官方开放了一个指针类型 unsafe.Pointer
,它可以包含任意类型变量的地址,它绕开了 Go 语言的类型系统,通过它可以直接操作内存,因此使用起来并不安全,接下来我们就一起看看 unsafe.Pointer
是什么,以及用它都玩些什么花活。
unsafe.Pointer
是特别定义的一种指针类型,它可以包含任意类型变量的地址。
代码地址:src/unsafe/unsafe.go,源码中只有 5 行代码,其余都是注释。
type ArbitraryType int
type Pointer *ArbitraryType
func Sizeof(x ArbitraryType) uintptr
func Offsetof(x ArbitraryType) uintptr
func Alignof(x ArbitraryType) uintptr
ArbitraryType
是 int
的一个别名,意思为任意的类型; Pointer
是 *ArbitraryType
的一个别名,表示指向任意类型的指针。 Go 官方文档对这个类型有如下四个描述:
unsafe.Pointer
;unsafe.Pointer
可以被转化为任何类型的指针;uintptr
可以被转化为 unsafe.Pointer
;unsafe.Pointer
可以被转化为 uintptr
。什么是 uintptr
?
源码地址:src/builtin/builtin.go
// uintptr is an integer type that is large enough to hold the bit pattern of
// any pointer.
type uintptr uintptr
uintptr
是一个整数类型,足够大能保存任何一种指针类型。uintptr
指的是具体的内存地址,不是个指针,因此 uintptr
地址关联的对象可以被垃圾回收。而 unsafe.Pointer 有指针语义,可以保护它不会被垃圾回收。
指针类型、unsafe.Pointer、uintptr 三者关系如下:
指针类型 *T <-> unsafe.Pointer <-> uintptr
uintptr
是可用于存储指针的整型,而整型是可以进行数学运算的。因此,将 unsafe.Pointer
转化为 uintptr
类型后,就可以让本不具备运算能力的指针具备了指针运算能力。
unsafe 包中还有三个函数,这里简单解释一下:
Sizeof
:返回类型 x 所占据的字节数,但不包含 x 所指向的内容的大小。例如,如果 x 是切片,则 Sizeof 返回切片描述符的大小,而不是切片引用的内存的大小。Offsetof
:返回 x 表示的字段在结构体中的偏移量,该字段必须采用 structValue.field
形式。 换句话说,它返回结构体开头和字段开头之间的字节数。Alignof
:返回变量 x 所需的对齐方式,例如;如果变量 s 是结构体类型,并且 f 是该结构体中的字段,则 Alignof(s.f)
将返回结构体中该类型字段所需的对齐方式。总结:
unsafe.Pointer
可以和任意的指针类型进行转换,意味着可以借助 unsafe.Pointer
完成不同指针类型之间的转换。unsafe.Pointer
可以转换为 uintptr
,而 uintptr
拥有计算能力,因此指针可以借助 unsafe.Pointer
和 uintptr
完成算术运算,进而直接操作内存。了解了 unsafe.Pointer
是什么后,我们通过实际的例子对其加深一下印象。
unsafe.Pointer
可以在不同的指针类型之间做转化,从而可以表示任意可寻址的指针类型,利用 unsafe.Pointer
为中介,即可完成指针类型的转换。 先举一个简单例子,看一下不同指针类型之间的转换过程:
package main
import (
"fmt"
"unsafe"
)
func main() {
i := 100
intI := &i
var floatI *float64
floatI = (*float64)(unsafe.Pointer(intI))
*floatI = *floatI * 3
fmt.Printf("%T\n", i)
fmt.Println(i)
fmt.Printf("%T\n", intI)
fmt.Printf("%T\n", floatI)
}
// 输出
int
300
*int
*float64
该例子中定义了两个指针变量分别是 *int
类型的 intI
和 *float64
类型的 floatI
,然后先对 intI
做了类型 unsafe.Pointer
的转换,随后进行 *float64
类型的转换;然后对 *float64
进行乘法操作,最终影响到了 i
变量,也从侧面证明了 *float64
的指针变量是指向 i
变量的内存地址的。
然后我们看一个类型转换经典的例子:实现字符串和 bytes 切片之间的转换,要求是零拷贝。 如果按照以往的方式,循环遍历,然后挨个拷贝赋值是无法完成目标的,这个时候只能考虑共享底层 []byte 数组才可以实现零拷贝转换。 string 和 []byte 在运行时的类型表示为 reflect.StringHeader
和 reflect.SliceHeader
源码:src/reflect/value.go
type StringHeader struct {
Data uintptr
Len int
}
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
使用 unsafe.Pointer
将 string
或 []byte
转换为 *reflect.StringHeader
或 *reflect.SliceHeader
,然后通过构造方式,完成底层 []byte 数组的共享,最后通过指针类型转换方式再次转换回来,代码如下:
func string2bytes(s string) []byte {
stringHeader := (*reflect.StringHeader)(unsafe.Pointer(&s))
bh := reflect.SliceHeader{
Data: stringHeader.Data,
Len: stringHeader.Len,
Cap: stringHeader.Len,
}
return *(*[]byte)(unsafe.Pointer(&bh))
}
func bytes2string(b []byte) string{
sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&b))
sh := reflect.StringHeader{
Data: sliceHeader.Data,
Len: sliceHeader.Len,
}
return *(*string)(unsafe.Pointer(&sh))
}
通过上边的分析,我们知道指针可以借助 unsafe.Pointer
和 uintptr
完成指针运算,接下来我们看两个例子。
案例一:通过指针运算,修改数组内部的值。
package main
import (
"fmt"
"unsafe"
)
func main() {
arr := [3]int{1, 2, 3}
ap := &arr
arr0p := (*int)(unsafe.Pointer(ap))
arr1p := (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(ap)) + unsafe.Sizeof(arr[0])))
*arr0p += 10
*arr1p += 20
fmt.Println(arr) // [11 22 3]
}
解释一下关键代码: arr0p := (*int)(unsafe.Pointer(ap))
通过类型转换的方式,使得 arr0p 变量指向了 arr 数组的起始地址,也就是 arr[0] 所在的起始地址;
uintptr(unsafe.Pointer(ap))
将数组的起始地址转换为 uintptr 类型,然后加上unsafe.Sizeof(arr[0])
数组第一个元素的偏移量,得到 arr[1] 的起始地址,通过类型转换为 *int 指针,赋值给 arr1p。最终做到通过指针修改数组内部元素的值。
案例二:通过指针运算,修改结构体内的值。
package main
import (
"fmt"
"unsafe"
)
type user struct {
name string
age int
}
func main() {
u := new(user)
fmt.Println(*u) // { 0}
pName := (*string)(unsafe.Pointer(u))
*pName = "张三"
pAge := (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(u)) + unsafe.Offsetof(u.age)))
*pAge = 20
fmt.Println(*u) // {张三 20}
}
通过 unsafe.Offsetof(u.age)
内存偏移的方式,定位到我们需要操作的字段(比如: age),然后改变它们的值。具体的类型转换过程和案例一类似,这里不再赘述。
unsafe.Pointer
有两个最重要的作用:
uintptr
突破指针不能进行算术运算的限制,从而达到直接操作内存的目的。unsafe.Pointer
类型可以绕开 Go 语言的类型系统,还可以直接操作内存,方便了很多代码的编写,且提升了代码性能,比如:底层类型相同的指针之间的转换,访问结构体私有字段等。但同时因为其特性,使其变得很不安全,因此请各位慎用。
阅读量:512
点赞量:0
收藏量:0