帅气的反射可以帮助我们做很多事情,但是它的性能常常成为瓶颈,在这种时候,我们就可以考虑使用 unsafe 来提升性能
内存和unsafe
unsafe 会直接操作内存,这是一种非常底层的能力,同时由于你可以基于内存做很多事,这也导致它非常不安全。
起始地址和偏移量
要想操作内存,我们必须知道你操作的变量(通常就是类的实例)的起始地址,以及各个字段的偏移量,知晓了这些,我们才可以在正确的位置上进行操作。
我们可以通过一些操作获取到我们想要的值
type Student struct {
Name string
Gender string
Age int64
}
func TestOffset(t *testing.T) {
s := Student{}
s.Name = "Peter"
s.Age = 33
// 获取起始地址,注意使用指针
startAddr := reflect.ValueOf(&s).UnsafePointer()
fmt.Println(startAddr)
// 获取地址
nameAddr := reflect.ValueOf(&s.Name).UnsafePointer()
fmt.Println(nameAddr)
// 获取地址
genderAddr := reflect.ValueOf(&s.Gender).UnsafePointer()
fmt.Println(genderAddr)
// 获取偏移量
fmt.Println(unsafe.Offsetof(s.Age))
// 获取地址
ageAddr := reflect.ValueOf(&s.Age).UnsafePointer()
fmt.Println(ageAddr)
}
//0x14000117470
//0x14000117470
//0x14000117480
//32
//0x14000117490
内存对齐
以直觉来说,一个字段的偏移量应该等于这个字段前的其他字段的大小之和,例如上面的的例子中,string 需要16字节,那么 Age 的偏移量就应该是 16 + 16 = 32,程序的实际结果也支持这个结论。
但是,在有些时候却并非如此
type XX struct {
A int32
B string
}
func TestAlign(t *testing.T) {
x := XX{1, "2"}
fmt.Println(unsafe.Sizeof(x.A)) // 4
fmt.Println(unsafe.Offsetof(x.B)) // 8
}
什么情况?为什么 int32 大小为 4,而 B 的偏移量是 8?
实际上,这和操作系统有关,通常我们的操作系统为 64 位,也就是说操作系统操作内存都是以 8 字节为基础进行操作。当我们定义了一个大小为 4 字节的字段实,如果我们以 4 作为偏移量去操作后续的字段,会对操作系统管理内存带来诸多不便,所以 go 会将剩余的 4 字节填充,以达到对齐内存的目的。
由于对齐的存在,使得使用 size 计算内存偏移会出现差错,所以建议直接获取 offset,省心省力
读写
读
这里提供两种方式读取内存:
- 直接赋值
func TestReadMemo(t *testing.T) {
x := int64(-10086)
ptr := unsafe.Pointer(&x)
y := *(*int64)(ptr) // 注意类型为 int64,你可以试一试将类型改为 uint64 会出现什么情况
fmt.Println(y) // -10086
}
- 使用反射
反射和 unsafe 是一对好伙伴,要好好利用
func TestReadMemoByRef(t *testing.T) {
x := int64(10086)
typ := reflect.TypeOf(x)
y := reflect.NewAt(typ, unsafe.Pointer(&x)).Elem()
fmt.Println(y.Interface()) // 10086
x = 123
fmt.Println(y.Interface()) // 123
}
写
- 直接修改
func TestWri(t *testing.T) {
x := int64(10086)
ptr := unsafe.Pointer(&x)
*(*int64)(ptr) = 123
fmt.Println(x)
}
- 使用反射
func TestWriRef(t *testing.T) {
x := int64(10086)
y := int64(666)
newX := reflect.NewAt(reflect.TypeOf(x), unsafe.Pointer(&x)).Elem()
if newX.CanSet() {
fmt.Println("ok") // ok
newX.Set(reflect.ValueOf(y))
}
fmt.Println(x) // 666
}
unsafe.Pointer 和 uintptr 的区别
- unsafe.Pointer:代表指针,并且 Pointer 会被 GC 管理
- uintptr:也可以代表指针,但是它的底层是一个 uint,不会被 GC 管理
有一个例子帮助你理解:
func TestPtr(t *testing.T) {
s := make([]int, 0, 1)
p1 := unsafe.Pointer(&s)
p2 := uintptr(p1)
s = append(s, 1)
fmt.Println(*(*[]int)(p1))
fmt.Println(*(*[]int)(unsafe.Pointer(p2)))
s = append(s, 2)
fmt.Println(*(*[]int)(p1))
fmt.Println(*(*[]int)(unsafe.Pointer(p2)))
}
//[1]
//[1]
//[1 2]
//[1]
应用
只看上面的例子,你可能觉得这种代码就是反人类。下面就用一个例子,来展示一下它的使用:
package unsafe_
import (
"errors"
"reflect"
"unsafe"
)
type FieldAccessor interface {
Field(field string) (int, error)
FieldAny(field string) (interface{}, error)
SetField(field string, val int) error
SetFieldAny(field string, val interface{}) error
}
type UnsafeAccessor struct {
entityAddr unsafe.Pointer
fields map[string]fieldMeta
}
type fieldMeta struct {
offset uintptr
typ reflect.Type
}
func NewFieldAccessor(entity interface{}) (FieldAccessor, error) {
if entity == nil {
return nil, errors.New("invalid entity")
}
typ := reflect.TypeOf(entity)
if typ.Kind() != reflect.Ptr || typ.Elem().Kind() != reflect.Struct {
return nil, errors.New("invalid entity")
}
typ = typ.Elem()
fields := make(map[string]fieldMeta, typ.NumField())
for i := 0; i < typ.NumField(); i++ {
field := typ.Field(i)
fields[field.Name] = fieldMeta{
offset: field.Offset,
typ: field.Type,
}
}
val := reflect.ValueOf(entity)
return &UnsafeAccessor{
entityAddr: val.UnsafePointer(),
fields: fields,
}, nil
}
func (u *UnsafeAccessor) Field(field string) (int, error) {
f, has := u.fields[field]
if !has {
return 0, errors.New("不存在字段")
}
res := *(*int)(unsafe.Pointer(uintptr(u.entityAddr) + f.offset))
return res, nil
}
func (u *UnsafeAccessor) FieldAny(field string) (interface{}, error) {
f, has := u.fields[field]
if !has {
return 0, errors.New("不存在字段")
}
res := reflect.NewAt(f.typ, unsafe.Pointer(uintptr(u.entityAddr)+f.offset)).Elem()
return res.Interface(), nil
}
func (u *UnsafeAccessor) SetField(field string, val int) error {
f, has := u.fields[field]
if !has {
return errors.New("不存在字段")
}
*(*int)(unsafe.Pointer(uintptr(u.entityAddr) + f.offset)) = val
return nil
}
func (u *UnsafeAccessor) SetFieldAny(field string, val interface{}) error {
f, has := u.fields[field]
if !has {
return errors.New("不存在字段")
}
res := reflect.NewAt(f.typ, unsafe.Pointer(uintptr(u.entityAddr)+f.offset))
if res.CanSet() {
res.Set(reflect.ValueOf(val))
}
return nil
}
你可以想象这样的场景:对于一个变量,我们不知道它有什么字段,但是我们想要获取这个变量中的特定字段,或者设置它。
面对这样的场景,你会怎么办呢?也许我们使用接口或其他方式可以处理这个问题(实际上如果可以不用反射和 unsafe,就不要用),但总会遇到一些这样那样的限制,那就可以尝试使用上面的代码,将这个变量包装为一个 FieldAccessor,这样你就可以对任何结构进行 field 操作了。
这里有一些测试用例
package unsafe_
import (
"errors"
"fmt"
"github.com/stretchr/testify/assert"
"reflect"
"testing"
"unsafe"
)
type User struct {
Age int
}
func TestNewFieldAccessor(t *testing.T) {
tests := []struct {
name string
entity interface{}
field string
wantVal int
wantErr error
}{
{
name: "invalid field",
field: "xxx",
entity: &User{
Age: 18,
},
wantErr: errors.New("不存在字段"),
},
{
name: "invalid entity",
field: "xxx",
entity: User{
Age: 18,
},
wantErr: errors.New("invalid entity"),
},
{
name: "normal case",
field: "Age",
entity: &User{19},
wantVal: 19,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
accessor, err := NewFieldAccessor(tt.entity)
if err != nil {
assert.Equal(t, tt.wantErr, err)
return
}
val, err := accessor.FieldAny(tt.field)
if err != nil {
assert.Equal(t, tt.wantErr, err)
return
}
assert.Equal(t, tt.wantVal, val)
})
}
}
这个例子可能很粗糙,但确实是一个我们可以使用 reflect 和 unsafe 的场景