• Welcome to Journal web site.

我是 PHP 程序员

- 开发无止境 -

Next
Prev

讲讲什么是 Golang 中的反射(通俗易懂)

Data: 2020-12-22 21:14:17Form: JournalClick: 25

前言

反射 —— 如果你之前学过一些别的语言,比如java可能就会了解,反射是一个传说中很厉害的操作,算是一个高级用法。而同时,很多人也会告诉你,反射是一个危险的操作,那么在golang中,反射又是怎么操作的呢?今天就来说说golang中的反射reflect。

关于反射(reflect )在 Golang 中文标准库中是这样介绍的

 reflect包实现了运行时反射,允许程序操作任意类型的对象。典型用法是用静态类型interface{}保存一个值,通过调用TypeOf获取其动态类型信息,该函数返回一个Type类型值。调用ValueOf函数返回一个Value类型值,该值代表运行时的数据。Zero接受一个Type类型参数并返回一个代表该类型零值的Value类型值。


抽取出来我们看到以下几点:

 1. 运行时,允许程序操作任意类型的对象

正常情况下,我们操作一个结构体或者对象里的字段或方法时,我们都需要率先知道我们要操作的结构体和对象是什么类型,只有知道是什么类型后才能调用。虽然我们可以对拿到的数据进行类型转换,但是在类型转换的过程中,不依然还是指定了要转成的类型吗,因此反射就是在程序运行的过程中,可以对一个未知类型的数据进行操作的过程,一句话来说就是啥也不知道,但就是要用
 
 2. interface{}保存一个值
 既然要用,那么如何使用呢?解释里面说的很清楚,通过 interface{} 空接口存储要处理未知类型的数据,那为什么要用 interface{} 来接收呢,答案是因为 Golang 中的空接口没有定义任何方法,任何类型变量都实现空接口,因此用空接口可以表示任意数据类型
 
 3. ValueOf,TypeOf函数
 通过调用上面俩个函数,就可以获得 Value 类型值和 Type 类型值,Value 代表这个未知类型里面的数据,可以通过函数对数据进行操作,Type 代表这个未知类型代表着数据类型,比如 int、string、指针、结构体 Student 等。
关于反射的具体落实,主要用于框架的开发,而一般开发中相对较少,毕竟在框架中对于操作的数据很多情况下都是未知的。

 

反射的定义

首先问问自己,你知道什么是反射吗?如果你有一个清楚的定义,证明你已经对反射非常熟悉了。
官方的定义很官方,我就说说我的:
反射,反射,从字面理解就是通过镜子(或类似的东西)看到自己。
而在编程中,反射指的是在运行的过程中看到自己。
在实际的编程过程中我们知道,创建的这个变量或者对象是什么类型或者是什么样子的,同时很容易能对它进行操作。而在运行过程中,程序没有我们的眼睛,它并不知道这个东西是怎么样的,这个时候就需要运用到反射。
通过反射我可以知道自己长什么样子。

反射的使用

reflect.TypeOf

如果你对反射还是有些模糊,那么看下面这个最简单的例子

func main() {
    a := 1.3
    fmt.Println("a的类型是", reflect.TypeOf(a))
}

输出

a的类型是 float64

是不是瞬间明白了,没错,反射没有那么复杂。
你想想,作为程序自己,我运行中,我怎么知道a是什么类型,只能通过照镜子(反射)得到。

下面再说说一些更高级的用法。

reflect.ValueOf

func main() {
    type MyInt int
    var x MyInt = 7
    v := reflect.ValueOf(x)
    fmt.Println(v.Type())
    fmt.Println(v.Kind())
}

输出

main.MyInt
int

这里我们通过reflect.ValueOf方法拿到的v,其中v.Type()拿到的是它当前的类型,而v.Kind()可以拿到它最基本的类型。

Elem()

func main() {
    a := 1.3
    v := reflect.ValueOf(&a)
    elem := v.Elem()
    elem.SetFloat(0.2)
    fmt.Println(a)
}

输出

0.2

这里我们可以看到,通过反射拿到v使用v.Elem()方法可以拿到对应指针进行操作赋值

Field()

type MyData struct {
    A int
    b float32
} 

func main() {
    myData := MyData{
        A: 1,
        b: 1.1,
    }
    myDataV := reflect.ValueOf(&myData).Elem()
    fmt.Println("字段a:", myDataV.Field(0))
    fmt.Println("字段b:", myDataV.Field(1))
    fmt.Println(myDataV)
    myDataV.Field(0).SetInt(2)
    fmt.Println(myDataV)
}

输出

字段a: 1
字段b: 1.1
{1 1.1}
{2 1.1}

这里我们可以看到,我们即使不知道一个结构体里面的情况,我们依旧可以通过Field方法获得其中的值,并且如果这个变量可以被外界访问那么还可以修改。

Interface()

func main() {
    a := 1.3
    v := reflect.ValueOf(a)
    a1 := v.Interface().(float64)
    fmt.Println(a1)
}

1.3

a的类型是 float64

反射之后的对象通过Interface还可以转换回来

反射的法则

上面就是一些反射的基本用法,常用的就是获取一个对象在运行中的一个状态,或者是针对运行中的一个不确定的对象进行修改。下面要说的是反射的法则。
如果你英文够好,并且网络自由,可以看看golang官方的博客:
https://blog.golang.org/laws-of-reflection
里面详细描述了反射的三个法则,如果你看不到,那就只能听我下面吹一吹了。

  • Reflection goes from interface value to reflection object.
  • Reflection goes from reflection object to interface value.
  • To modify a reflection object, the value must be settable.

这三个就是官方给出的法则,我分别用自己的话解释一下。

反射就是将任意值转换为反射对象

在golang中我们知道interface就和java中的Object类似(只是类似而已),代表了所有类型,reflect包正是帮我们将任意的一个类型转换成了我们上面例子中看到的一个v,这个v就是反射对象。
通过这个反射对象中的一些方法我们才能看见原来的对象是什么样子。

反射对象可以转换为任意对象

这个正好与第一个相反,就像最后一个例子中给出的,反射获得的反射对象可以通过Interface方法转换为原来的对象。

如果你要修改反射对象,那么这个对象必须要可以被修改

什么意思呢?就如同这个案例中

func main() {
    a := 1.3
    v := reflect.ValueOf(&a)
    elem := v.Elem()
    elem.SetFloat(0.2)
    fmt.Println(a)
}

如果我们传递的并非a的地址并且直接使用v.SetFloat那么就会报错,因为我们无法对其进行修改,反射会帮我们copy一个,所以无法修改,只有当我们使用指针的时候才能修改。

同样的,和案例中的结构体操作一样,如果一个结构体的变量是小写的而不是大写的,证明外界没有办法访问到,所以也没有办法修改,也会出现异常。

反射的原理

下面就需要看看在源码中,反射到底是怎么实现的了。
我们着重看两个方法TypeOfValueOf

TypeOf

// TypeOf returns the reflection Type that represents the dynamic type of i.
// If i is a nil interface value, TypeOf returns nil.
func TypeOf(i interface{}) Type {
    eface := *(*emptyInterface)(unsafe.Pointer(&i))
    return toType(eface.typ)
}

我们先来看这个简单的TypeOf看到源码中很简单,通过unsafe.Pointer获得指针转换成emptyInterface类型

// emptyInterface is the header for an interface{} value.
type emptyInterface struct {
    typ  *rtype
    word unsafe.Pointer
}

然后通过toType方法得到具体类型

func toType(t *rtype) Type {
    if t == nil {
        return nil
    }
    return t
}

其中Type就包含了所有的信息,然后返回出去就完成了。

ValueOf

// ValueOf returns a new Value initialized to the concrete value
// stored in the interface i. ValueOf(nil) returns the zero Value.
func ValueOf(i interface{}) Value {
    if i == nil {
        return Value{}
    }

    // TODO: Maybe allow contents of a Value to live on the stack.
    // For now we make the contents always escape to the heap. It
    // makes life easier in a few places (see chanrecv/mapassign
    // comment below).
    escapes(i)

    return unpackEface(i)
}

上面nil就不说了,主要方法是下面unpackEface

// unpackEface converts the empty interface i to a Value.
func unpackEface(i interface{}) Value {
    e := (*emptyInterface)(unsafe.Pointer(&i))
    // NOTE: don't read e.word until we know whether it is really a pointer or not.
    t := e.typ
    if t == nil {
        return Value{}
    }
    f := flag(t.Kind())
    if ifaceIndir(t) {
        f |= flagIndir
    }
    return Value{t, e.word, f}
}

我们可以看到其实与TypeOf一样,只不过多封装了一层Value,其中的word就是当前对象的指针,因为我们知道通过TypeOf得到的Value可以用很多操作。

// emptyInterface is the header for an interface{} value.
type emptyInterface struct {
    typ  *rtype
    word unsafe.Pointer
}

反射的意义

说了这么多,那么反射存在的意义到底在哪?说白了,我们在写代码的时候什么时候能用上它?
还是举个例子你就明白了。

json.Marshal案例

json.Marshal这个方法用过吧,是将任意对象转换成json,这个案例就足以说明反射的厉害了。
我们先自己想一下,如果要将一个对象转换成json:

  • 我们运行之前其实是不知道传入对象的类型,而且传入的对象不同,那么解析方式肯定不同,如果传入的是map或者传入的是struct肯定解析方式不同,所以方法内部需要动态的判断传入类型从而做操作。
  • 还有我们不知道传入的struct内部的属性长什么样子。

这个时候反射就能解决这样的问题,通过反射可以知道传入对象的类型,根据不同的类型做操作,同时可以获取到如struct这样类型内部的字段属性和值分别是多少。

json.Marshal源码分析

因为所有源码太多,我给出查看路线,然后给出上面所述的两处重点。
json.Marshal -> e.marshal -> e.reflectValue -> valueEncoder -> typeEncoder -> newTypeEncoder -> newStructEncoder -> se.encode

要点1 - newTypeEncoder

// newTypeEncoder constructs an encoderFunc for a type.
// The returned encoder only checks CanAddr when allowAddr is true.
func newTypeEncoder(t reflect.Type, allowAddr bool) encoderFunc {
    ...........

    switch t.Kind() {
    case reflect.Bool:
        return boolEncoder
    case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
        return intEncoder
    case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
        return uintEncoder
    case reflect.Float32:
        return float32Encoder
    case reflect.Float64:
        return float64Encoder
    case reflect.String:
        return stringEncoder
    case reflect.Interface:
        return interfaceEncoder
    case reflect.Struct:
        return newStructEncoder(t)
    case reflect.Map:
        return newMapEncoder(t)
    case reflect.Slice:
        return newSliceEncoder(t)
    case reflect.Array:
        return newArrayEncoder(t)
    case reflect.Ptr:
        return newPtrEncoder(t)
    default:
        return unsupportedTypeEncoder
    }
}

通过反射获得传入对象的类型,判断选择具体的编码器进行编码,如果传入的是map那就...如果传入的是struct那就...

要点2 - se.encode

func (se *structEncoder) encode(e *encodeState, v reflect.Value, opts encOpts) {
    e.WriteByte('{')
    first := true
    for i, f := range se.fields {
        fv := fieldByIndex(v, f.index)
        if !fv.IsValid() || f.omitEmpty && isEmptyValue(fv) {
            continue
        }
        if first {
            first = false
        } else {
            e.WriteByte(',')
        }
        e.string(f.name, opts.escapeHTML)
        e.WriteByte(':')
        opts.quoted = f.quoted
        se.fieldEncs[i](e, fv, opts)
    }
    e.WriteByte('}')
}
func fieldByIndex(v reflect.Value, index []int) reflect.Value {
    for _, i := range index {
        if v.Kind() == reflect.Ptr {
            if v.IsNil() {
                return reflect.Value{}
            }
            v = v.Elem()
        }
        v = v.Field(i)
    }
    return v
}

encode这个方法是解析结构体的,我们可以清楚的看的从结构体中通过v.Field将里面的参数拿出来。
其他细节这里就不做说明了,主要的目的是要表示反射在其中起到的重要作用。

总结和提醒

看完你就应该清楚反射到底是做什么用的,具体我们什么时候会用到它。最后还要提醒一下,反射也存在两个必然的问题:

  • 第一个是不安全,因为反射的类型在转换中极易出现问题,所以使用需谨慎。
  • 第二个是速度慢,之所以有人抨击golang的json解析库慢,一部分原因就是因为其中涉及到了反射,所以如果对效率有要求的地方就要斟酌使用了。
Name:
<提交>