5 4
golang reflect实践

之前看过很多Go技术的文章和一些技术群里面的交流,主流意见都是认为反射(reflect)是导致性能的主要元凶,叫开发者少用为妙;主要是他们使用反射的业务场景离不开下面的情况:

  • reflect涉及到内存分配以及后续的GC

  • reflect实现里面有大量的枚举,也就是for循环,比如类型之类的

一开始我也“遵循”的,直到最近项目需要,越来越多的场景需要用到反射机制才能灵活应对,毕竟某些情况某些情景下性能不是特别高的要求,模块的灵活性扩展性(够有能力处理各种输入类型,而这些类型可能无法共享同一个接口,也可能布局未知)才是占据重要的位置。反射不但指计算机程序在运行时(Run time)可以访问、检测和修改它本身状态或行为的一种能力,也是元编程的一种形式,我们常见的RPC库或者一些ORM框架基本都是使用反射来实现核心的功能,所以本文主要是最近使用反射的一些总结

Go的类型系统

Go不是面向对象的语言,所以它的类型系统特点是没有泛型、没有继承的,所有的类型都是一个interface。

首先Go的类型系统主要是静态类型的,主要分两种:

  • 一种是预声明类型/pre-declared type,在创建变量的时候已经确定具体类型。go默认的有几个预声明类型:boole,num,string type。这些预声明类型被用来构造其他的类型。

  • 一种是字面量类型/type literal(array,struct,pointer,slice,map,channel,function,interface)

注意:type literal 中的struct,interface 特指匿名的类型,没有使用type包装,

每一个类型都有一个底层类型(underlying type)

在静态类型的基础上,Go提供了interface机制,它使得Go这样的静态语言拥有了一定的动态性,但却又不损失静态语言在类型安全方面拥有的编译时检查的优势。interface让我们程序设计中做到依赖于接口而不是实现,优先使用组合而不是继承,这是Go程序开发中抽象的方案。

而reflect主要是面向interface相关的类型,interface类型的变量总是具有相同的静态类型(具有某种底层类型),即使在运行时在接口变量中的值可能改变,但该值始终满足接口

Go语言中反射的操作主要定义在标准库reflect中,在标准库中定义了两种类型来表现运行时的对象信息,分别是:reflect.Type(反射对象的类型)和 reflect.Value(反射对象的值),所有反射操作都是基于这两个类型进行的。

为了说明reflect处理类型的相关知识,我们先看一个例子:

file, err := os.Open("/tmp/test.log")
if err != nil {
    panic(err)
}
defer file.Close()

var r io.Reader
r = file
fmt.Printf("%v\n", reflect.TypeOf(r))

var w io.Writer
w = r.(io.Writer)
fmt.Printf("%v\n", reflect.TypeOf(w))

程序输出

*os.File
*os.File

reflect.TypeOf 函数接受任何的interface{}参数,并且把接口中的动态类型以reflect.Type的形式返回;把一个具体指赋值给一个接口类型的时候会发生一个隐式类型的转换,转换会生成一个包含两部分内容的接口值:

因为reflect.TypeOf返回一个接口值对应的动态类型,所以它返回的总是具体类型(而不是接口类型)。比如上面例子执行输出的是*.os.File,而不是 io.Writer,这点很重要!

reflect另外一个重要操作是valueOf, 它是用来获取参数接口值得数据的动态值,当执行reflect.valueOf后,可以通过它本身的Interface()方法来获取接口的真实内容,再通过类型判断,就可以获取真实内容的值了。

Go反射的基本操作

说到Go的反射操作,先说两点:

  • 反射必须结合interface才行

  • 变量的type必须是concrete才行

interface底层有两部分:type 和 value,我们统称为pair。type是interface变量的一个指针指向的值的类型信息。value是interface变量的另外一个指针指向的实际的值

interface及其(type,value)的存在是Go反射的前提,也就是说Go的反射终归是用来检测存储在interface内部(type,value)的机制。

那么如何通过使用reflect.Value来设置值?

我们使用反射除了运行时判断真实类型,无非也是想在运行时更新变量的值

再看一个例子:

a := 12
b := reflect.ValueOf(&a)
b.Set(reflect.ValueOf(int64(48)))
fmt.Println(a)

上面的代码,我们目的是在运行时把变量a的值从12修改为48,但单当我们运行代码的时候,会抛出异常

panic: reflect: reflect.Value.Set using unaddressable value

unaddressable ? 也就是上面我们的变量b其实是unaddressable的,哪怕b有一个指针指向变量a,但无法这样的方式来更新变量a的值。

那么如何让b变为addressable呢? 再来看下面的示范:

x := 12
a := reflect.ValueOf(2)
println(a.CanAddr()) //false

b := reflect.ValueOf(x)
println(b.CanAddr()) //false

c := reflect.ValueOf(&x)
println(c.CanAddr()) //false

d := reflect.ValueOf(&x).Elem()
println(d.CanAddr()) //true

a不可取址,因为a中的值仅仅是12的拷贝的副本

b不可取址,因为b中的值是a的拷贝的副本

c不可取址,因为c中的值只是一个指针&x的拷贝

d可取址,因为Elem()方法,可以获取变量对应的可取址地址的value

官方对Elem()的解释是:

Elem returns the value that the interface v contains

所以,上面由于只有d可取值,也就是只有可寻址的reflect.Value直接调用reflect.Value.Set来更新值,否则会抛出以下的异常:

panic: reflect: reflect.Value.SetInt using unaddressable value

ok,也就是说我们可以通过Elem()来让我们某些不可取址的value变为addressable,那么我再重写一下上面的代码:

a := 12
b := reflect.ValueOf(&a).Elem()
b.Set(reflect.ValueOf(int64(48)))
fmt.Println(a)

当我们以为一切正常的时候,这个时候,我们又得到一个异常信息:

panic: reflect.Set: value of type int64 is not assignable to type int

从这个异常看出b现在是addressable了,但却变成了unassignable了!为什么呢?

我们回顾下,interface类型的变量总是具有相同的静态类型,也就是说我们通过反射来对interface进行设值,这个值的静态类型必须是相同的,否则会分配不成功,我们再更改下例子的代码:

a := 12
b := reflect.ValueOf(&a).Elem()
b.Set(reflect.ValueOf(48))
fmt.Println(a)

这个时候,变量a重要成功被更新了,所以可以总结go反射操作变量需要满足几个条件:

1、反射可以将“接口类型变量”转换为“反射类型对象”

2、反射可以将“反射类型对象”转换为“接口类型变量”

3、如果要修改“反射类型对象”,其值必须是“可写的”、“可址的”

若对go reflect 赋值操作感兴趣的,可以阅读文章《learning to use go reflection》
若要更深入了解Go语言的reflect,可以阅读官方的《laws of reflection》

反射实践

一、slice类型处理

我们尝试思考下如何可以接收任意类型slices,并统一返回[]interface{}?

也许你可能会说:“这不是很容易吗?直接处理就行了”,于是你可能会这样处理:

func handleSlice(in []interface{}) (out []interface{}) {
    out = make([]interface{}, len(in))
    for i, v := range in {
        out[i] = v
    }
    return
}

于是我们尝试把一个[]int的slice作为入参的时候,还没到运行时,我们已经收到报错信息:

cannot use slice (type []int) as type []interface {} in argument to handleSlice4

原因很简单,我们的入参类型是[]int,它只能作为一个interface的隐藏类型,但不能作为[]interface的隐藏类型,所以上面的处理方法是不恰当的,我们应该这样写:

func handleSlice(arg interface{}) (out []interface{}, ok bool) {
    argValue := reflect.ValueOf(arg)
    if argValue.Type().Kind() == reflect.Slice {
        length := argValue.Len()
        if length == 0 {
            return
        }
        ok = true
        out = make([]interface{}, length)
        for i := 0; i < length; i++ {
            out[i] = argValue.Index(i).Interface()
        }
    }
    return
}

二、类型转换

我们使用反射的一个比较常见的功能是用于类型的转换,如下面的代码片段,我们目的是要把一个未知类型的值转化为字符串并返回

stringutil.go

... ...

func ToStr(value interface{}, args ...int) (s string) {
    switch v := value.(type) {
    case bool:
        s = strconv.FormatBool(v)
    case float32:
        s = strconv.FormatFloat(float64(v), 'f', argInt(args).Get(0, -1), argInt(args).Get(1, 32))
    case float64:
        s = strconv.FormatFloat(v, 'f', argInt(args).Get(0, -1), argInt(args).Get(1, 64))
    case int:
        s = strconv.FormatInt(int64(v), argInt(args).Get(0, 10))
    case int8:
        s = strconv.FormatInt(int64(v), argInt(args).Get(0, 10))
    case int16:
        s = strconv.FormatInt(int64(v), argInt(args).Get(0, 10))
    case int32:
        s = strconv.FormatInt(int64(v), argInt(args).Get(0, 10))
    case int64:
        s = strconv.FormatInt(v, argInt(args).Get(0, 10))
    case uint:
        s = strconv.FormatUint(uint64(v), argInt(args).Get(0, 10))
    case uint8:
        s = strconv.FormatUint(uint64(v), argInt(args).Get(0, 10))
    case uint16:
        s = strconv.FormatUint(uint64(v), argInt(args).Get(0, 10))
    case uint32:
        s = strconv.FormatUint(uint64(v), argInt(args).Get(0, 10))
    case uint64:
        s = strconv.FormatUint(v, argInt(args).Get(0, 10))
    case string:
        s = v
    case []byte:
        s = string(v)
    default:
        s = fmt.Sprintf("%v", v)
    }
    return s
}

... ...

再比方说我们日常最常用的“技巧” --- 字符串与byte数组互相转换

func BytesToString(b []byte) string {
    bh := (*reflect.SliceHeader)(unsafe.Pointer(&b))
    sh := reflect.StringHeader{bh.Data, bh.Len}
    return *(*string)(unsafe.Pointer(&sh))
}

func StringToBytes(s string) []byte {
    sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
    bh := reflect.SliceHeader{sh.Data, sh.Len, 0}
    return *(*[]byte)(unsafe.Pointer(&bh))
}

上面例子两个 reflect 例子, 我们以 BytesToString为测试对象,看下一些基准测试:

func BenchmarkDirectConvert(b *testing.B) {
    for i := 0; i < b.N; i++ {
        b := []byte("I am gopher")
        _ = string(b)
    }
}

func BenchmarkReflectConvert(b *testing.B) {
    for i := 0; i < b.N; i++ {
        b := []byte("I am gopher")
        _ = BytesToString(b)
    }
}

//运行结果:

$ go test -v -bench=. -benchmem

BenchmarkDirectConvert-4        100000000            10.8 ns/op           0 B/op           0 allocs/op
BenchmarkReflectConvert-4       200000000             6.78 ns/op           0 B/op           0 allocs/op

可以看到reflect的使用在某些情况下,非但没有引起性能问题,侧面还提升了呢,所以避开场景让reflect背上性能的锅都是不客观的。

三、单机版的MapReduce

下面是通过反射实现一个简单的单机版简单的类MR演示

package main

import (
    "fmt"
    "reflect"
)

func Map(slice interface{}, fn func(a interface{}) interface{}) interface{} {
    val := reflect.ValueOf(slice)
    out := reflect.MakeSlice(reflect.TypeOf(slice), val.Len(), val.Cap())
    for i := 0; i < val.Len(); i++ {
        ret := fn(val.Index(i).Interface())
        out.Index(i).Set(reflect.ValueOf(ret))
    }
    return out.Interface()
}

func main() {
    a := []int{1, 2, 3}
    fn := func(a interface{}) interface{} {
        return a.(int) * 2
    }
    b := Map(a, fn)
    fmt.Printf("%v\n", b)
}

四、reflect 缓存

实现reflect.Type池主要的为了某程度提升reflect的性能,避免重复操作耗时

下面是一个屏蔽用户名字展示的测试例子:

type User struct {
    Name string
    Age  int
}

var handler = func(u *User, message string) {
    fmt.Printf("Hello, My name is %s, I am %d years old ! so, %s\n", u.Name, u.Age, message)
}

//使用普通反射的方式处理名字屏蔽
func filtName(u *User, message string) {
    fn := reflect.ValueOf(handler)
    uv := reflect.ValueOf(u)
    name := uv.Elem().FieldByName("Name")
    name.SetString("XXX")
    fn.Call([]reflect.Value{uv, reflect.ValueOf(message)})
}


//重用部分数据减少重复创建的反射方式处理名字屏蔽
var offset uintptr = 0xFFFF
func filtNameWithReuseOffset(u *User, message string) {
    if offset == 0xFFFF {
        t := reflect.TypeOf(u).Elem()
        name, _ := t.FieldByName("Name")
        offset = name.Offset

    }
    up := (*[2]uintptr)(unsafe.Pointer(&u))
    upnamePtr := (*string)(unsafe.Pointer(up[0] + offset))
    *upnamePtr = "YYY"
    fn := reflect.ValueOf(handler)
    uv := reflect.ValueOf(u)
    fn.Call([]reflect.Value{uv, reflect.ValueOf(message)})
}

//压测数据:

BenchmarkFiltName-4              5000000               376 ns/op              56 B/op          3 allocs/op
BenchmarkFiltNameWithCache-4     5000000               268 ns/op              48 B/op          2 allocs/op

虽然不明显,但通过对部分数据的重用可以起到优化反射的作用,优化效果还是要看具体的业务实现常见, 当然如果可以把上面的缓存方法设计得更通用些,可以这样:

var cache = map[*uintptr]map[string]uintptr{}

func filtNameWithCache(u *User, message string) {
    itab := *(**uintptr)(unsafe.Pointer(&u))

    m, ok := cache[itab]
    if !ok {
        m = make(map[string]uintptr)
        cache[itab] = m
    }

    offset, ok := m["Name"]
    if !ok {
        t := reflect.TypeOf(u).Elem()
        name, _ := t.FieldByName("Name")
        offset = name.Offset
        m["Name"] = offset
    }
    up := (*[2]uintptr)(unsafe.Pointer(&u))
    upnamePtr := (*string)(unsafe.Pointer(up[0] + offset))
    *upnamePtr = "ZZZ"
    fn := reflect.ValueOf(handler)
    uv := reflect.ValueOf(u)
    fn.Call([]reflect.Value{uv, reflect.ValueOf(message)})

}

除了以上场景,基于Go的数据库查询结果封装转换也是比较常见的手段;可以参考 scanner.go

总结

Go的反射(reflect)是一个强大并富有表达力的工具,一般情况下它应该被小心地使用,原因有几点:

  • 如果通过反射实现的功能,则是在真正运行到的时候才会抛出panic异常

  • 反射的操作不能做静态类型检查,而且大量反射的代码可读性较差。

  • 一般场景基于反射的代码通常比正常的代码运行速度要慢

不过,关于上面第三点,尽管反射存在性能问题,但依然被频繁使用,以弥补静态语言在动态行为上的不足。某些时候,我们可以配合一些手段,以提升性能