Unsafe的基本使用

91 阅读3分钟

要理解 unsafe,核心就是要理解 Go中一个对象在内存中究竟是怎么布局的。需要掌握:

  • 计算地址
  • 计算偏移量
  • 直接操作内存

读写字段

unsafe 操作的是内存,本质上是对象的起始地址。如果想要操作一个对象,首先要知道对象的起始地址。

*(*T)(ptr),T是目标类型,如果类型不知道,只能拿到反射的 Type即 refelect.Type ,那么可以用reflect.NewAt(typ, ptr).Elem()

type FieldMeta struct {
   typ    reflect.Type // 字段类型
   offset uintptr      // 字段偏移量
}

type UnsafeAccessor struct {
   fields     map[string]FieldMeta // 对象的字段信息
   entityAddr unsafe.Pointer       // 对象的起始地址
}

如上面的代码片段所示,我们新建了一个 UnsafeAccessor 用于存储对象的结构信息。FieldMeta 用于存储对象的每个字段的类型以及相对于起始地址的偏移量。

func NewUnsafeAccessor(entity any) (*UnsafeAccessor, error) {
   typ := reflect.TypeOf(entity)
   // 传入参数只能为指针类型
   if typ.Kind() != reflect.Ptr || typ.Elem().Kind() != reflect.Struct {
      return nil, errors.New("invalid entity")
   }
   typ = typ.Elem()

   nums := typ.NumField()
   fields := make(map[string]FieldMeta, nums)
   for i := 0; i < nums; i++ {
      fd := typ.Field(i)
      fields[fd.Name] = FieldMeta{
         offset: fd.Offset,
         typ:    fd.Type,
      }
   }

   val := reflect.ValueOf(entity)
   return &UnsafeAccessor{
      fields:     fields,
      entityAddr: val.UnsafePointer(),
   }, nil
}

定义了 NewUnsafeAccessor 函数用于根据传入的对象创建出 UnsafeAccessor,这里我们对传入的实体类型做出了限制(只能传入指针),因为 unsafe 是操作内存的,如果传入的是非指针类型即为值传递,随着函数的调用完成,该实体的内存就随着函数栈的弹出回收掉了,UnsafeAccessor 中记录的内存地址就变得毫无意义,这一点需要尤其注意。

func (u *UnsafeAccessor) Field(name string) error {
   meta, ok := u.fields[name]
   if !ok {
      return errors.New("invalid field")
   }

   // a := *(*string)(unsafe.Pointer(uintptr(u.entityAddr) + meta.offset))
   // fmt.Println(a)

   val := reflect.NewAt(meta.typ, unsafe.Pointer(uintptr(u.entityAddr)+meta.offset)).Elem()
   res := val.Interface()
   fmt.Println(res)
   return nil
}

上面代码片段中的 Field 方法用于读取实体的具体字段值,读取值方法就是对象起始地址 + 偏移地址 具体的读取方法分为两种:

  • 知道字段类型:当知道具体类型的时候,我们可以直接进行显示的类型转换,例如该字段类型是 string ,那么读取该字段值的方式就是 *(*string)(unsafe.Pointer(起始地址 + 偏移量)) ,当知道字段类型的时候推荐使用该方法
  • 不知道字段具体类型:不知道具体类型的时候,需要调用 reflect.NewAt 方法来读取字段的值,需要注意的一点是,reflect.NewAt 方法的返回值是一个指针,我们需要调用 Elem 方法拿到具体的 reflect.Value

了解了读之后,写就变得非常简单:

  • *(*值类型)(unsafe.Pointer(起始地址 + 偏移量)) = 值
  • 在不知道值类型的时候,我们依然可以使用 reflect.NewAt ,但是我们需要调用 CanSet 加以判断:
val := reflect.NewAt(meta.typ, unsafe.Pointer(起始地址 + 偏移地址))
if val.CanSet() {
    val.Set(reflect.ValueOf(值))
}

unsafe.Pointer 与 uintptr

前面我们使用了 unsafe.Pointeruintptr,这两个都代表指针,那么他们有什么区别呢?

  • unsafe.Pointer:是Go层面的指针,是一个抽象的概念,GC会维护 unsafe.Pointer 的值,因为Go有自己的垃圾回收机制,对于同一个对象,经过一轮GC之后,虽然它没有被GC回收,但是它可能已经被GC移到另一片内存空间中了,如果我们直接去访问内存中的物理地址,可能会出现错误;而 unsafe.Pointer 由GC维护,经过GC之后,GC会修改它新的内存地址
  • uintptr:就是一串数字,代表的是内存的地址,一般用于地址计算