golang unsafe

帅气的反射可以帮助我们做很多事情,但是它的性能常常成为瓶颈,在这种时候,我们就可以考虑使用 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,省心省力

读写

这里提供两种方式读取内存:

  1. 直接赋值
func TestReadMemo(t *testing.T) {
    x := int64(-10086)
    ptr := unsafe.Pointer(&x)

    y := *(*int64)(ptr)  // 注意类型为 int64,你可以试一试将类型改为 uint64 会出现什么情况
    fmt.Println(y)  // -10086
}
  1. 使用反射
    反射和 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
}

  1. 直接修改
func TestWri(t *testing.T) {
    x := int64(10086)
    ptr := unsafe.Pointer(&x)

    *(*int64)(ptr) = 123
    fmt.Println(x)
}
  1. 使用反射
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 的场景

你可能感兴趣的:(golang unsafe)