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