在开始介绍Go的反射机制前,需要发起两个灵魂发问。

反射是什么?

反射就是程序能够在运行时检查变量和值,求出它们的类型。

为什么需要反射?

我们经常会把函数的参数定义为空interface类型(比如下面这段代码),编译时我们并不知道传入test_type的是什么类型的数据,传入的数据的具体类型是运行期确定的。因为任何类型都可以声称自己实现了interface{},因此我们在函数传参时可以往该函数的参数里塞各种类型的数据。如果我们无法获取传入的interface的真实类型,那我们也就没法针对数据做出我们的逻辑处理了。

  1. package main
  2. // main.go
  3. import (
  4. "fmt"
  5. )
  6. func test_type(v interface {}) {
  7. fmt.Println("value:", v)
  8. }
  9. func main() {
  10. var x float64 = 9.0
  11. var y int64 = 999
  12. test_type(x)
  13. test_type(y)
  14. }

下面开始介绍Go的反射机制和特性。

1. 反射可以将interface类型变量转换为反射对象

如果我们希望知道传入的interface的具体类型,再根据其真实类型来做不同的处理,这里Go是怎么提供支持的呢?这里就会用到Go的反射机制,即使用reflect包来提供获取传入的v的实际数据类型和数值。

比如下面这个例子,我们首先使用reflect.TypeOf获取inteface的具体类型,再使用t.Kind()来获取真实的数据类型,最后根据k的值做不同的逻辑处理。因此,我们在实现一个供外界调用的函数接口时,我们可以把函数参数定义为空interface,即允许调用方传入任何类型的数据,我们再利用反射机制确定运行期传入的真实的数据类型,然后再根据类型做不同的逻辑处理。

package main

// main.go

import (
    "fmt"
    "reflect"
)

type User struct {
    Name string
    Age int
}

func test_type(v interface {}) {
    t := reflect.TypeOf(v)
    s := reflect.ValueOf(v)
    k := t.Kind()
    fmt.Println("raw data:", v)
    fmt.Println("type:", t)
    fmt.Println("value:", s)
    fmt.Println("kind:", k)

    switch k {
    case reflect.Int64:
        fmt.Println("type is int64");
    case reflect.Float64:
        fmt.Println("type is float64");
    default:
        fmt.Println("unknown type");
    }
    fmt.Println("===================")
}

func main() {
    var x float64 = 9.2
    var y int64 = 999
    m := map[int]string {1:"james", 2:"ken"}
    u := User{"KK", 1}
    test_type(x)
    test_type(y)
    test_type(m)
    test_type(u)
}

输出

raw data: 9.2
type: float64
value: 9.2
kind: float64
type is float64
===================
raw data: 999
type: int64
value: 999
kind: int64
type is int64
===================
raw data: map[1:james 2:ken]
type: map[int]string
value: map[1:james 2:ken]
kind: map
unknown type
===================
raw data: {KK 1}
type: main.User
value: {KK 1}
kind: struct
unknown type
===================

interface转反射对象的应用场景很多,一个著名的场景就是HTTP服务器解析HTTP请求,因为请求的类型千奇百怪,我们把请求解析函数的参数定义为interface{},也就是支持任意数据类型的传入,再使用反射的特性获取传入数据的具体类型,根据类型做不同的处理。Gin的请求解析代码如下,同样也是使用了反射特性解析请求,可以参考一下。

https://github.com/gin-gonic/gin/blob/34ce2104cad324f444943c528746bf6d23643cd3/binding/default_validator.go#L37

func (v *defaultValidator) ValidateStruct(obj interface{}) error {
    if obj == nil {
        return nil
    }

    value := reflect.ValueOf(obj)
    switch value.Kind() {
    case reflect.Ptr:
        return v.ValidateStruct(value.Elem().Interface())
    case reflect.Struct:
        return v.validateStruct(obj)
    case reflect.Slice, reflect.Array:
        count := value.Len()
        validateRet := make(sliceValidateError, 0)
        for i := 0; i < count; i++ {
            if err := v.ValidateStruct(value.Index(i).Interface()); err != nil {
                validateRet = append(validateRet, err)
            }
        }
        if len(validateRet) == 0 {
            return nil
        }
        return validateRet
    default:
        return nil
    }
}

2. 将对象转化为interface对象

反射的第二个能力,能够将从一个反射对象还原为原来的interface对象。下面的例子中,x,y,u,m都先转化为反射对象,然后再转化为interface。

package main

// main.go

import (
    "fmt"
    "reflect"
)

type User struct {
    Name string
    Age int
}

func main() {
    var x,y,u,m interface{}
    x = 9.2
    y = 999
    u = User{"KK",20}
    m = map[int]string{1:"james"}

    x1 := reflect.ValueOf(x)
    y1 := reflect.ValueOf(y)
    u1 := reflect.ValueOf(u)
    m1 := reflect.ValueOf(m)

    x2 := x1.Interface() 
    y2 := y1.Interface() 
    u2 := u1.Interface()
    m2 := m1.Interface()

    if reflect.DeepEqual(x, x2) && reflect.DeepEqual(y, y2) && reflect.DeepEqual(u, u2) &&reflect.DeepEqual(m, m2) {
        fmt.Println("all equal!!!")
    }

    fmt.Println("x2=", x2)
    fmt.Println("y2=", y2)
    fmt.Println("u2=", u2)
    fmt.Println("m2=", m2)
}

输出

all equal!!!
x2= 9.2
y2= 999
u2= {KK 20}
m2= map[1:james]

3. 设置反射对象的值

上面的两个例子都只用到反射对象的读操作,如果我们需要对反射对象进行值修改,应该怎么操作呢?

我们可以使用反射对象的SetFloat等方法来设置,注意传入函数的参数是指针类型,这样才能支持函数内修改数据,如果传入的是值类型但又直接在test_type里修改,就会引发panic。所以一定要注意,通过反射可以修改interface的值,但前提是必须获得interface的变量地址。

package main

// main.go

import (
    "fmt"
    "reflect"
)

func test_type(v interface {}) {
    s := reflect.ValueOf(v)
    t := reflect.TypeOf(v)
    s.Elem().SetFloat(7.1)
    fmt.Println("type=", t)
    fmt.Println("kind=", t.Kind())
}

func main() {
    var x float64 = 9.2

    test_type(&x)

    fmt.Println("x=", x)
}

输出

type= *float64
kind= ptr
x= 7.1

4. 利用反射特性读写复杂结构体

上面读写反射对象的场景都过于简单,然后我们日常面对的数据往往是复杂的结构体,里面内嵌着map,slice,struct等各种复杂类型的数据,因此我们需要掌握如何对复杂结构体进行读写。

下面这个例子,准备对这个User结构体进行反射处理,因为这个结构体内定义了map,array,struct等类型,比较符合我们日常处理的结构体的复杂度。

type Address struct {
    City string
}

type User struct {
    Name string
    Age int
    Score map[string]int
    Friends *[5]string
    Addr *Address
}

这个例子定义了record函数,利用反射特性,处理传入的各种类型,处理内容就是对数据进行读和写。

package main

// main.go

import (
    "fmt"
    "reflect"
)

type Address struct {
    City string
}

type User struct {
    Name string
    Age int
    Score map[string]int
    Friends *[5]string
    Addr *Address
}

func record(u interface{}) {
    t := reflect.TypeOf(u)
    v := reflect.ValueOf(u)
    fmt.Println("record t", t)
    fmt.Println("record v", v)

    user := v.Elem()
    typeOfT := user.Type()
    //read
    for i := 0; i < user.NumField(); i++ {
        f := user.Field(i)
        fmt.Printf("%d: %s %s = %v\n", i,
            typeOfT.Field(i).Name, f.Type(), f.Interface())
    }

    //可以直接根据结构体的字段名来读数据
    name := user.FieldByName("Name")
    fmt.Println(name.String())

    //write
    //写基础类型
    user.FieldByName("Age").SetInt(100)

    //写map
    sc := user.FieldByName("Score").Interface()
    fmt.Println("Score:",sc)
    md, _ := sc.(map[string]int)
    md["Math"] = 100
    md["PE"] = 60

    // 写array
    friends := user.FieldByName("Friends").Interface().(*[5]string)
    fmt.Println("friends:",friends)
    friends[2] = "Sam"
    friends[3] = "Tody"

    // 写内嵌结构体
    addr := user.FieldByName("Addr").Interface().(*Address)
    fmt.Println("addr:",addr)
    addr.City = "Zhuhai"
}

func main() {

    u := User{"KK", 20, map[string]int{"Math":70}, &[5]string{"Tom", "Ken"}, &Address{"Guangzhou"}}

    record(&u)

    fmt.Println("record done, u=", u)
    fmt.Println("record done, u.Friends", u.Friends)
    fmt.Println("record done, u.Score", u.Score)
    fmt.Println("record done, u.Addr", u.Addr)
}

输出

record t *main.User
record v &{KK 20 map[Math:70] 0xc0000b8000 0xc00008e1e0}
0: Name string = KK
1: Age int = 20
2: Score map[string]int = map[Math:70]
3: Friends *[5]string = &[Tom Ken   ]
4: Addr *main.Address = &{Guangzhou}
KK
Score: map[Math:70]
friends: &[Tom Ken   ]
addr: &{Guangzhou}
record done, u= {KK 100 map[Math:100 PE:60] 0xc0000b8000 0xc00008e1e0}
record done, u.Friends &[Tom Ken Sam Tody ]
record done, u.Score map[Math:100 PE:60]
record done, u.Addr &{Zhuhai}

5. 利用反射特性做函数回调

结构体不仅可以定义数据,还可以定义方法,我们可以利用反射特性实现函数调用,比如这里我们实现一个回调函数,当record被调用完成后,会执行传入的callback函数,实现函数回调。

这里注意三点:

  • MethodByName方法可以让我们根据一个方法名获取一个方法对象,然后我们构建好该方法需要的参数,最后调用Call就达到了动态调用方法的目的。
  • 获取到的方法我们可以使用IsValid 来判断是否可用(存在)。
  • 的参数是一个Value类型的数组,所以需要的参数,我们必须要通过ValueOf函数进行转换。
package main

// main.go

import (
    "fmt"
    "reflect"
)

type User struct {
    Name string
    Age int
    Score int
}

func (u *User) Callback(name string, score int) {
    //do something
    fmt.Printf("Callback !!! name=%s, score=%d\n", name, score)
}

func record(u interface{}) {
    t := reflect.TypeOf(u)
    v := reflect.ValueOf(u)
    fmt.Println("record t", t)
    fmt.Println("record v", v)

    user := v.Elem()
    typeOfT := user.Type()
    //do something
    for i := 0; i < user.NumField(); i++ {
        f := user.Field(i)
        fmt.Printf("%d: %s %s = %v\n", i,
            typeOfT.Field(i).Name, f.Type(), f.Interface())
    }

    user.Field(2).SetInt(100)

    fCallback := v.MethodByName("Callback")
    if fCallback.IsValid() {
        args := []reflect.Value{reflect.ValueOf(user.Field(0).Interface()), reflect.ValueOf(user.Field(2).Interface())}
        fCallback.Call(args)
    }
}

func main() {

    u := User{"KK",20, 0}

    record(&u)

    fmt.Println("record done, u=", u)
}

输出

record t *main.User
record v &{KK 20 0}
0: Name string = KK
1: Age int = 20
2: Score int = 0
Callback !!! name=KK, score=100
record done, u= {KK 20 100}

6.反射值得使用吗

赞成派:

  • 有时你需要编写一个函数,但是并不知道传给你的参数类型是什么,可能是没约定好;也可能是传入的类型很多,这些类型并不能统一表示。这时反射就会用的上了。(HTTP请求解析,ORM)
  • 有时候需要根据某些条件决定调用哪个函数,比如根据用户的输入来决定。这时就需要对函数和函数的参数进行反射,在运行期间动态地执行函数。(函数回调,异步通知)

反对派:

  • 与反射相关的代码,经常是难以阅读的。在软件工程中,代码可读性也是一个非常重要的指标。抽象的代码总是不好理解的,虽然代码很简洁。因为传入的参数类型是未知的,因此函数维护者很难能提前预测到数据风险,进而难以做出更为合适的防御性编程。
  • Go 语言作为一门静态语言,编码过程中,编译器能提前发现一些类型错误,但是对于反射代码是无能为力的。所以包含反射相关的代码,很可能会运行很久,才会出错,这时候经常是直接 panic,可能会造成严重的后果。比如读写复杂结构体的例子中,万一user.FieldByName("Name")这里的字段名称写错为”name”,可不会编译不过,而是运行时才给你抛panic,让你程序直接挂掉。
  • 反射对性能影响还是比较大的,比正常代码运行速度慢一到两个数量级。所以,对于一个项目中处于运行效率关键位置的代码,尽量避免使用反射特性。