参考官方文档解释:unsafe包 字节对齐文章:https://www.yuque.com/rzzdy/nl7dm1/qtqxiq

unsafe包含关于go语言的类型安全的一些操作,该包内就一个unsafe文件,文件内只有2个type,3个func

type ArbitraryType int

  • 可代表任意类型的意思
  • 这里的ArbitraryType只是提供文档作用,并不是真实unsafe包的一部分,只是代表了go表达式的任意类型。

    type Pointer *ArbitraryType

1、Pointer代表指向任意类型的指针。Pointer支持四种操作:

  • 任何类型的指针都可以转成Pointer
  • 一个Pointer可以转换成任何一种类型指针
  • 一个uintptr可以转成一个Pointer
  • 一个Pointer可以转成一个uintptr

2、利用上面的4条规则,Pointer的下列操作都是合法的
(1)指针T1转到T2
假定T1小于T2,并且这两个变量共享内存布局,则允许将一种类型转换为另一种类型。比如math.Float64bits的实现:

  1. func Float64bits(f float64) uint64 {
  2. return *(*uint64)(unsafe.Pointer(&f))
  3. }

(2)将Pointer转换为uintpr
将Pointer转换为指定类型值的内存地址,作为整数存储。此种转换创建的uintptr是没有指针语义的整数,基本都是为了打印,无实际意义。uintpr只是持有了某个对象的地址值,gc不会因为对象变化更新uintptr的值,也不会通过uintptr回收该对象。

  1. h := Human{
  2. sex:true, // 1
  3. age: 20, // 1
  4. // 补充6字节
  5. min: 11, // 8
  6. name: "123", // 8+8
  7. }
  8. pt := (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&h)) + unsafe.Offsetof(h.min)))
  9. fmt.Println(*pt)
  10. *pt = 30
  11. fmt.Println(*pt)
  1. 11
  2. 30

上述例子将h结构体转成uintptr,然后通过计算h.min的偏移量,再用Pointer转成了int指针。 第一次打印是默认值11,第二次因为pt指向了h.min,所以就是修改的原值,打印30. 这里也说明了使用unsafe包的不安全性,可以通过指针转换修改值

下面几个是从uintptr转到Pointer唯一合法方式

(3)对uintptr进行算数运算再转成Pointer

  • 还是上面那个例子里的代码:
    1. pt := unsafe.Pointer(uintptr(unsafe.Pointer(&h)) + unsafe.Offsetof(h.min)))

&h先转成uintptr再进行算数运算,等价于

  1. pt := unsafe.Pointer(&h.min)
  • 不像C,指针所指变量,对其超出预先分配的内存块进行计算是不合法的

比如:

  1. var s string
  2. end := unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + unsafe.Sizeof(s))

上面end指针指向的超出了变量分配的内存空间,所以invalid。但end指针本身是合法的

又比如:

  1. b := make([]byte, 10)
  2. end := unsafe.Pointer(uintptr(unsafe.Pointer(&b[0])) + uintptr(10))
  3. 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
    1. u := unsafe.Pointer(nil)
    2. p := unsafe.Pointer(uintptr(u) + 10)

(4)直接进行系统调用时,Pointer可以转成uintptr传递给操作系统。

  1. syscall.Syscall(syscall.SYS_READ, uintptr(fd), uintptr(unsafe.Pointer(p)), uintptr(n))

注意只能在参数里使用uintptr,不可单独定义变量,理由如第(3)里。

(5)使用reflect.Value.Pointer()或reflect.Value.UnsafeAddr()将uintptr转到Pointer

  1. p := (*int)(unsafe.Pointer(reflect.ValueOf(new(int)).Pointer()))

还是注意uintptr不能存储在变量里

(6)通过reflect.SliceHeader或reflect.StringHeader的Data域与Pointer进行相互转换

  1. ss := "123"
  2. var s string
  3. hdr := (*reflect.StringHeader)(unsafe.Pointer(&s))
  4. hdr.Data = uintptr(unsafe.Pointer(&ss))
  5. 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是在编译阶段进行计算的,不是在运行时,而切片是动态扩容的,所以编译阶段只有描述符。

比如:

  1. var a = []int{1, 2, 3, 4, 5}
  2. fmt.Println(unsafe.Sizeof(a))
  1. 24

上例是个切片,返回的是描述符的大小

还有

  1. var a = [...]int{1, 2, 3, 4, 5}
  2. fmt.Println(unsafe.Sizeof(a))
  1. 40

上例是数组,返回的是数组实际内存大小。因为数组是固定大小的,编译阶段就能确定size。

2、结构体
结构体是要按字节对齐的【字节对齐可参考:https://www.yuque.com/rzzdy/nl7dm1/qtqxiq

  1. h := Human{
  2. sex:true, // 1
  3. age: 20, // 1
  4. // 补充6字节
  5. min: 11, // 8
  6. name: "123", // 字符串描述符大小,8字节指针+8字节长度=16
  7. }
  8. fmt.Println(unsafe.Sizeof(h))
  1. 32

上面结构体补齐后输出32字节

3、字符串
字符串内部是用一个结构体表示,包含两部分:指向字符串实际值内存的指针,表示字符串长度的整型,各占8字节,总共16字节。

源码可参考reflect/value.go的定义:

  1. type StringHeader struct {
  2. Data uintptr
  3. Len int
  4. }

如下例子:

  1. s := "123"
  2. fmt.Println(unsafe.Sizeof(s))
  1. 16

因为字符串也是可动态扩容的,所以编译阶段只能返回结构体的大小。