参考官方文档解释:unsafe包 字节对齐文章:https://www.yuque.com/rzzdy/nl7dm1/qtqxiq
unsafe包含关于go语言的类型安全的一些操作,该包内就一个unsafe文件,文件内只有2个type,3个func
type ArbitraryType int
1、Pointer代表指向任意类型的指针。Pointer支持四种操作:
- 任何类型的指针都可以转成Pointer
- 一个Pointer可以转换成任何一种类型指针
- 一个uintptr可以转成一个Pointer
- 一个Pointer可以转成一个uintptr
2、利用上面的4条规则,Pointer的下列操作都是合法的
(1)指针T1转到T2
假定T1小于T2,并且这两个变量共享内存布局,则允许将一种类型转换为另一种类型。比如math.Float64bits的实现:
func Float64bits(f float64) uint64 {
return *(*uint64)(unsafe.Pointer(&f))
}
(2)将Pointer转换为uintpr
将Pointer转换为指定类型值的内存地址,作为整数存储。此种转换创建的uintptr是没有指针语义的整数,基本都是为了打印,无实际意义。uintpr只是持有了某个对象的地址值,gc不会因为对象变化更新uintptr的值,也不会通过uintptr回收该对象。
h := Human{
sex:true, // 1
age: 20, // 1
// 补充6字节
min: 11, // 8
name: "123", // 8+8
}
pt := (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&h)) + unsafe.Offsetof(h.min)))
fmt.Println(*pt)
*pt = 30
fmt.Println(*pt)
11
30
上述例子将h结构体转成uintptr,然后通过计算h.min的偏移量,再用Pointer转成了int指针。 第一次打印是默认值11,第二次因为pt指向了h.min,所以就是修改的原值,打印30. 这里也说明了使用unsafe包的不安全性,可以通过指针转换修改值
下面几个是从uintptr转到Pointer唯一合法方式
(3)对uintptr进行算数运算再转成Pointer
- 还是上面那个例子里的代码:
pt := unsafe.Pointer(uintptr(unsafe.Pointer(&h)) + unsafe.Offsetof(h.min)))
&h先转成uintptr再进行算数运算,等价于
pt := unsafe.Pointer(&h.min)
- 不像C,指针所指变量,对其超出预先分配的内存块进行计算是不合法的
比如:
var s string
end := unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + unsafe.Sizeof(s))
上面end指针指向的超出了变量分配的内存空间,所以invalid。但end指针本身是合法的
又比如:
b := make([]byte, 10)
end := unsafe.Pointer(uintptr(unsafe.Pointer(&b[0])) + uintptr(10))
fmt.Println(*end)
- uintptr和Pointer计算必须同时出现在同一语句里,uintptr不能单独作为变量存储。
go var s string var p = unsafe.Pointer(&s) u := uintptr(p) p = unsafe.Pointer(u + 10)
上面是错误操作,原因是gc会进行移动变量来内存整理,当变量发生异动后,所有指向旧地址的指针都应该更新成新的。uintptr只是个整数值,在编译阶段就决定了,值不会变动。
而上面的代码使得gc无法通过变量u来了解所代表的的指针。p可能发生了移动,p的新地址与u的值不同,导致u所代表的值并不是原来的p的地址了。
- 指针必须指向已分配的对象,不能是nil
u := unsafe.Pointer(nil)
p := unsafe.Pointer(uintptr(u) + 10)
(4)直接进行系统调用时,Pointer可以转成uintptr传递给操作系统。
syscall.Syscall(syscall.SYS_READ, uintptr(fd), uintptr(unsafe.Pointer(p)), uintptr(n))
注意只能在参数里使用uintptr,不可单独定义变量,理由如第(3)里。
(5)使用reflect.Value.Pointer()或reflect.Value.UnsafeAddr()将uintptr转到Pointer
p := (*int)(unsafe.Pointer(reflect.ValueOf(new(int)).Pointer()))
还是注意uintptr不能存储在变量里
(6)通过reflect.SliceHeader或reflect.StringHeader的Data域与Pointer进行相互转换
ss := "123"
var s string
hdr := (*reflect.StringHeader)(unsafe.Pointer(&s))
hdr.Data = uintptr(unsafe.Pointer(&ss))
hdr.Len = 10
注意要使用*reflect.StringHeader指针类型,否则无法与Pointer进行互换 uintptr也不能预先存储,应该在表达式里直接使用,防止在对象移动导致地址发生变更时,无法及时更新uintptr所指向的指针值。
func Alignof(x ArbitraryType) uintptr
该函数返回的结果与reflect.TypeOf(x).Align()返回值相同,计算的是变量的对齐值。
当x为普通非结构体时计算的是x的地址的长度。
但当x为结构体时,计算的是变量在结构体内的对齐值。与reflect.TypeOf(x).FieldAlign()相同。
返回值是常数,即在编译阶段就计算好了。
func Offsetof(x ArbitraryType) uintptr
x必须为结构体成员,即structValue.field。
该函数返回该field在结构体内的偏移量,即从结构体开始到该字段的对齐之后的字节数。
返回值是常量,即在编译阶段就计算好了。
func Sizeof(x ArbitraryType) uintptr
关于该方法返回值
1、切片或数组if x is a slice, Sizeof returns the size of the slice descriptor, not the size of the memory referenced by the slice.
如果x是一个切片,sizeof返回的是切片的描述符的大小,不是切片所占内存的大小 因为sizeof是在编译阶段进行计算的,不是在运行时,而切片是动态扩容的,所以编译阶段只有描述符。
比如:
var a = []int{1, 2, 3, 4, 5}
fmt.Println(unsafe.Sizeof(a))
24
上例是个切片,返回的是描述符的大小
还有
var a = [...]int{1, 2, 3, 4, 5}
fmt.Println(unsafe.Sizeof(a))
40
上例是数组,返回的是数组实际内存大小。因为数组是固定大小的,编译阶段就能确定size。
2、结构体
结构体是要按字节对齐的【字节对齐可参考:https://www.yuque.com/rzzdy/nl7dm1/qtqxiq】
h := Human{
sex:true, // 1
age: 20, // 1
// 补充6字节
min: 11, // 8
name: "123", // 字符串描述符大小,8字节指针+8字节长度=16
}
fmt.Println(unsafe.Sizeof(h))
32
上面结构体补齐后输出32字节
3、字符串
字符串内部是用一个结构体表示,包含两部分:指向字符串实际值内存的指针,表示字符串长度的整型,各占8字节,总共16字节。
源码可参考reflect/value.go的定义:
type StringHeader struct {
Data uintptr
Len int
}
如下例子:
s := "123"
fmt.Println(unsafe.Sizeof(s))
16
因为字符串也是可动态扩容的,所以编译阶段只能返回结构体的大小。