如果你经常看 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) uintptrArbitraryType 是 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 uintptruintptr 是一个整数类型,足够大能保存任何一种指针类型。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 语言的类型系统,还可以直接操作内存,方便了很多代码的编写,且提升了代码性能,比如:底层类型相同的指针之间的转换,访问结构体私有字段等。但同时因为其特性,使其变得很不安全,因此请各位慎用。
阅读量:566
点赞量:0
收藏量:0