Go虽然具有指针,但出于安全性的设计,与C/C++的指针相比,存在诸多限制:
但Go团队并没有完全没收编程人员操纵指针的自由,提供了unsafe包的unsafe.Pointer类型作为非安全的指针,并提供了少量但足够的方法,让我们能像在C/C++上一样进行“无视类型”、“放飞自我”的指针操作。虽然,像包名一样,这种操作是不安全的、不被官方推荐的,但在某些场景中,直接操作指针将更加高效,在源码中,可以看到大量unsafe包的使用。
这个包的存在,也体现出Go团队努力兼顾安全和效率的实用主义设计理念。
因为实现直接关系到编译器源码,在此只总结一下该包的使用方法。
unsafe包中共包含了两个类型与三个函数:
type ArbitraryType int //代表着Go中任意类型
type Pointer *ArbitraryType //代表指向任意类型的指针,可以与任意*Type类型相互转换
func Sizeof(x ArbitraryType) uintptr //返回变量在内存中所占的字节数
func Offsetof(x ArbitraryType) uintptr //返回变量指定属性的偏移量
func Alignof(x ArbitraryType) uintptr //返回变量对齐字节数量
三个函数都是在编译期间执行,它们的结果可以直接复制给const类型变量,另外,因为三个函数执行的结果和操作系统、编译器相关,所以是不可移植的。
unsafe.Pointer类型可以与任何指针类型相互转换,并支持指针判等操作。
type object struct{
f float64
i int32
}
func main(){
obj:=&object{
f: 1.0,
i: 1,
}
f:=(*float64)(unsafe.Pointer(obj))
fmt.Println(*f) //1
fmt.Println(unsafe.Pointer(obj)==unsafe.Pointer(f)) //true
}
unsafe.Pointer类型仍然不支持数学运算,但可以将unsafe.Pointer类型转换为uintptr类型进行自由的数学计算。
// uintptr is an integer type that is large enough to hold the bit pattern of
// any pointer.
type uintptr uintptr
普通指针类型、unsafe.Pointer、uintptr存在如下的转换关系:
我们可以通过上述类型转换来达到指针运算的目的。
在下面的代码中,我们可以使用这种方式,用一个object实例的指针,得到第二个字段i的指针。
type object struct{
f float64
i int32
}
func main(){
obj:=&object{
f: 2.0,
i: 1,
}
fmt.Println(uintptr(unsafe.Pointer(obj))) //obj指向第一个字节的的内存实际地址824634236704
i:=(*int32)(unsafe.Pointer(uintptr(unsafe.Pointer(obj))+8)) //在第一个字节的基础上,加上一个fload64偏移量8
fmt.Println(*i) //1
}
即使在包外,即使字段是非导出的(首字母小写)或者说私有的,我们仍可以通过这种方式,从底层指针绕开Go的类型检查来操作变量。
unsafe.Sizeof函数返回变量在内存中占用的字节数,返回值为uintptr类型,可以直接用于偏移量的计算。该函数只会对变量基本结构进行分析,对于指针操作仅得指针大小,不能得到其指向数据的大小,对于slice、map、channel也是一样,并不能得到类型背后数据的总大小。
type object struct{
f float64
i int32
}
func main(){
obj:=object{
f: 2.0,
i: 1,
}
integer:=1
float:=1.0
// 对于非指针类型,获得这个类型在内存中占用空间大小
fmt.Println(unsafe.Sizeof(integer)," ",unsafe.Sizeof(float)," ",unsafe.Sizeof(obj)) //8 8 16
obj_ptr:=&obj
int_ptr:=&integer
f_ptr:=&float
//对于指针的操作,都将得到8字节(与操作系统相关)的指针大小
fmt.Println(unsafe.Sizeof(int_ptr)," ",unsafe.Sizeof(f_ptr)," ",unsafe.Sizeof(obj_ptr)) //8 8 8
//make新建的map,返回的是一个hmap的指针,因此长度仍然是一个指针长度
m:=make(map[int]int)
fmt.Println(unsafe.Sizeof(m)) //8
//新建的channel是一个hchan指针
ch:=make(chan int)
fmt.Println(unsafe.Sizeof(ch)) //8
//新建的slice将返回一个slice header,大小固定,与slice长度无关
s1:=make([]int,10)
s2:=make([]int,20)
fmt.Println(unsafe.Sizeof(s1)) //24
fmt.Println(unsafe.Sizeof(s2)) //24
//对于一个数组将返回占用的真实空间
array:=[24]int{}
fmt.Println(unsafe.Sizeof(array)) //192
}
前面我们可以看到,我们可以使用unsafe包操作一个结构体指针,获得指向其具体属性的指针:
type object struct{
f1 float64
f2 float64
s string
}
func main(){
obj_ptr:=&object{
f1: 2.0,
f2:2.0,
s:"helloworld",
}
s_ptr:=(*string)(unsafe.Pointer(uintptr(unsafe.Pointer(obj_ptr))+
unsafe.Sizeof(float64(0))+unsafe.Sizeof(float64(0))))
fmt.Println(*s_ptr) //helloworld
}
对于一个特定位置的字段,我们需要将前面所有的字段都考虑到来计算偏移量,这种工作比较繁杂。
对于可访问(在同一包内或者被导出)的字段类型,unsafe包提供了一种更优雅的方式:
type object struct{
f1 float64
f2 float64
s string
}
func main(){
obj_ptr:=&object{
f1: 2.0,
f2:2.0,
s:"helloworld",
}
s_ptr:=(*string)(unsafe.Pointer(uintptr(unsafe.Pointer(obj_ptr))+unsafe.Offsetof(obj_ptr.s)))
fmt.Println(*s_ptr) //helloworld
}
unsafr.Offsetof函数返回变量指定属性的偏移量,即指定字段在当前结构体中偏移大小,该函数的参数必需是struct类型的一个属性,因此,无法在包外对一个没有被导出的属性使用该方法。
修改上面的代码,为object字段设置一个int型属性,再尝试通过计算偏移量,获得s的指针。
type object struct{
f float64
i int32
s string
}
func main(){
obj_ptr:=&object{
f: 2.0,
i:1,
s:"helloworld",
}
s_ptr:=(*string)(unsafe.Pointer(uintptr(unsafe.Pointer(obj_ptr))+
unsafe.Sizeof(float64(0))+unsafe.Sizeof(int32(0))))
fmt.Println(*s_ptr)
}
结果将输出一个panic
runtime: VirtualAlloc of 42949672960 bytes failed with errno=1455
fatal error: out of memory
这是因为数据在内存中的存储并不是简单的排列 ,偏移量并不是简单的属性大小相加。
熟悉计算机组成和内存结构的人都知道,这里需要考虑一个对齐量的问题,其原理这里不再赘述。
使用unsafe.Alignof函数可以获得变量的对齐字节数量:
fmt.Println(unsafe.Alignof(obj_ptr))//8
根据对齐量来考虑属性的储存情况,才能计算到真正的偏移量。
type object struct{
f float64
i int32
s string
}
func main(){
obj_ptr:=&object{
f: 2.0,
i:1,
s:"helloworld",
}
fmt.Println(unsafe.Alignof(*obj_ptr))//8
fmt.Println(unsafe.Sizeof(float64(0)))//8
fmt.Println(unsafe.Sizeof(int32(0)))//4 //不足8,需要补齐
s_ptr:=(*string)(unsafe.Pointer(uintptr(unsafe.Pointer(obj_ptr))+
8+8))
fmt.Println(*s_ptr) //helloworld
}
reflect包也提供了函数来获得类型的对齐值:
fmt.Println(reflect.TypeOf(obj_ptr).Elem().Align())
runtime/slice.go中slice的结构:
type slice struct {
array unsafe.Pointer
len int
cap int
}
了解这个结构后,虽然len和cap并不是可以被访问的,我们仍然可以通过unsafe包修改:
func main(){
s:=make([]int,10)
fmt.Println(len(s)) //10
slen:=(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s))+uintptr(8)))
fmt.Println(*slen) //10
*slen=5
fmt.Println(len(s)) //5
}
map.go中hmap定义如下:
type hmap struct {
// Note: the format of the hmap is also encoded in cmd/compile/internal/gc/reflect.go.
// Make sure this stays in sync with the compiler's definition.
count int // # live cells == size of map. Must be first (used by len() builtin)
flags uint8
B uint8 // log_2 of # of buckets (can hold up to loadFactor * 2^B items)
noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details
hash0 uint32 // hash seed
buckets unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.
oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing
nevacuate uintptr // progress counter for evacuation (buckets less than this have been evacuated)
extra *mapextra // optional fields
}
可以看到,count字段被放在了hmap的第一个属性,因此,可以很容易通过unsafe包获得map的长度:
func main(){
m:=make(map[int]int)
m[0]=0
m[1]=0
m[2]=0
count:=**(**int)(unsafe.Pointer(&m))
fmt.Println(count) //3
}
可以看到,如果要转换的类型不能被识别为一个指针,我们可以通过二级指针来处理。
利用reflect包中的StringHeader和SliceHeader与string、slice的结构一致,我们可以巧妙地完成一次“高端转换”。
func string2bytes(s string) []byte {
stringHeader:=(*reflect.StringHeader)(unsafe.Pointer(&s))
byteSlice:=reflect.SliceHeader{
Data: stringHeader.Data,
Len: stringHeader.Len,
Cap: stringHeader.Len,
}
return *(*[]byte)(unsafe.Pointer(&byteSlice))
}
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))
}