顾名思义,unsafe 包是不安全的,为了安全起见我们尽量不要使用,不过 unsafe 也有优势便是可以绕过 go 的内存安全机制,直接对内存进行读写。所以有时候出于性能需要,还是会冒险使用它来对内存进行操作。接下来我将会介绍一系列 unsafe 包的应用。
指针类型转换
go 是一门强类型的静态语言,强类型意味着一旦定义了,类型就不能改变;静态意味着类型检查在运行前就做了。同时,go 是不允许两个指针类型进行转换的,比如 *int 不能转换为 *float64 。
package main
func main() {
v1 := 1
p1 := &v1
var p2 *float64 = (*float64)(p1)
}
运行这段代码,报错:cannot convert p1 (variable of type *int) to type *float64 ,这时我们便可以使用 unsafe 包里的 Pointer 进行转换。
unsafe.Pointer
unsafe.Pointer 可以表示任意类型的指针,类似于 c 语言中的 void* 。
正常情况下,*int 不能转换为 *float64 ,但如果我们先用 unsafe.Pointer 先转换一次就可以了。
func main() {
v1 := 1
p1 := &v1
var p2 *float64 = (*float64)(unsafe.Pointer(p1))
*p2 *= 2
fmt.Println(v1)
}
上面的代码是可以正常运行的,并且我们对 p2 操作,最终打印结果 v1 也发生了改变。
通过这个例子,我们可以知道,只需要借助 unsafe.Pointer 我们便可以实现任意指针类型间的相互转换,但有一点仍需知道,unsafe.Pointer 是不支持运算的,如果我们想要直接对指针进行运行,我们便需要使用到 unitptr 。
uintptr
() uintptr 也是一种指针类型,它也可以表示任意指针。
// uintptr is an integer type that is large enough to hold the bit pattern of
// any pointer.
type uintptr uintptr
下面的是使用指针偏移运算来实现修改结构体字段的例子:
type person struct {
Name string
Age int
}
func main() {
p := new(person)
//Name是person的第一个字段不用偏移,即可通过指针修改
pName := (*string)(unsafe.Pointer(p))
*pName = "tom"
//Age并不是person的第一个字段,所以需要进行偏移,这样才能正确定位到Age字段这块内存,才可以正确的修改
pAge := (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(p)) + unsafe.Offsetof(p.Age)))
*pAge = 20
fmt.Println(*p)
}
需要注意的是,如果要进行指针运算,要先通过 unsafe.Pointer 转换为 uintptr 类型的指针。指针运算完毕后,还要通过 unsafe.Pointer 转换为真实的指针类型(比如示例中的 *int 类型),这样可以对这块内存进行赋值或取值操作。
指针运算的核心在于它操作的是一个个内存地址,通过内存地址的增减,就可以指向一块块不同的内存并对其进行操作,而且不必知道这块内存被起了什么名字(变量名)。
unsage.Sizeof
Sizeof 返回一个类型所占内存的大小,与 c 语言的 sizeOf 相似
ps:一个 struct 结构体的内存占用大小,等于它包含的字段类型内存占用大小之和。
unsafe.Alignof
Alignof 返回一个类型的对齐值,也可以叫做对齐系数或者对齐倍数。对齐值是一个和内存对齐有关的值,合理的内存对齐可以提高内存读写的性能。
unsafe.Alignof(x)等价于reflect.TypeOf(x).Align()。
总结
使用 unsafe 包可以让你在 *T、uintptr 及 Pointer 三者间转换,达到例如零内存拷贝或通过 uintptr 进行指针运算的目的。
但是 unsafe 包会避开 go 语言编译器的检查,如果操作不熟练就有可能出现内存方面的问题。但有些情况还是建议使用,比如 []byte 与 string 的相互转换便是通过 unsafe.Pointer 实现。