码迷,mamicode.com
首页 > 编程语言 > 详细

10. Go 语言反射

时间:2019-12-22 10:36:26      阅读:135      评论:0      收藏:0      [点我收藏+]

标签:有符号   映射   inf   json   nic   unsafe   script   java ee   Staff   

Go 语言反射

反射是指在程序运行期对程序本身进行访问和修改的能力。程序在编译时,变量被转换为内存地址,变量名不会被编译器写入到可执行部分。在运行程序时,程序无法获取自身的信息。

支持反射的语言可以在程序编译期将变量的反射信息,如字段名称、类型信息、结构体信息等整合到可执行文件中,并给程序提供接口访问反射信息,这样就可以在程序运行期获取类型的反射信息,并且有能力修改它们。

Go程序在运行期使用reflect包访问程序的反射信息。
其它语言中的反射
C/C++ 语言没有支持反射功能,只能通过 typeid 提供非常弱化的程序运行时类型信息。Java、C# 等语言都支持完整的反射功能。

Lua、JavaScript 类动态语言,由于其本身的语法特性就可以让代码在运行期访问程序自身的值和类型信息,因此不需要反射系统。

Go 程序的反射系统无法获取到一个可执行文件空间中或者是一个包中的所有类型信息,需要配合使用标准库中对应的词法、语法解析器和抽象语法树(AST)对源码进行扫描后获得这些信息。

Go语言反射(reflection)简述

Go语言提供了一种机制在运行时更新变量和检查它们的值、调用它们的方法和它们支持的内在操作,但是在编译时并不知道这些变量的具体类型。这种机制被称为反射。反射也可以让我们将类型本身作为第一类的值类型处理。

反射(reflection)是在 Java 出现后迅速流行起来的一种概念。通过反射,你可以获取丰富的类型信息,并可以利用这些类型信息做非常灵活的工作。

在 Java 中,你可以读取配置并根据类型名称创建对应的类型,这是一种常见的编程手法。Java 中的很多重要框架和技术(比如 Spring/IoC、Hibernate、Struts)等都严重依赖于反射功能。虽然,使用 Java EE 时很多人都觉得很麻烦,比如需要配置大量 XML 格式的配置程序,但这毕竟不是反射的错,反而更加说明了反射所带来的高可配置性。

大多数现代的高级语言都以各种形式支持反射功能,除了一切以性能为上的 C++ 语言。Go语言的反射实现了反射的大部分功能,但没有像 Java 语言那样内置类型工厂,故而无法做到像 Java 那样通过类型字符串创建对象实例。

反射是把双刃剑,功能强大但代码可读性并不理想。若非必要,并不推荐使用反射,下面我们将介绍反射功能在 Go语言中的具体体现以及反射的基本使用方法。

基本概念

Go语言中的反射与其他语言有比较大的不同。首先我们要理解两个基本概念 Type 和 Value,它们也是 Go语言包中 reflect 空间里最重要的两个类型。我们先看一下下面的定义:

type MyReader struct {
    Name string
}
func (r MyReader)Read(p []byte) (n int, err error) {
    // 实现自己的Read方法
}

因为 MyReader 类型实现了 io.Reader 接口的所有方法(其实就是一个 Read() 函数),所以 MyReader 实现了接口 io.Reader。我们可以按如下方式来进行 MyReader 的实例化和赋值:

var reader io.Reader
reader = &MyReader{"a.txt"}

现在我们可以再来解释一下什么是 Type,什么是 Value。

对所有接口进行反射,都可以得到一个包含 Type 和 Value 的信息结构。比如我们对上例的 reader 进行反射,也将得到一个 Type 和 Value,Type 为 io.Reader,Value 为 MyReader{"a.txt"}。顾名思义,Type 主要表达的是被反射的这个变量本身的类型信息,而 Value 则为该变量实例本身的信息。

基本用法

通过使用 Type 和 Value,我们可以对一个类型进行各项灵活的操作。接下来我们分别演示反射的几种最基本用途。

1) 获取类型信息

为了理解反射的功能,我们先来看看下面代码所示的这个小程序。

package main
import (
    "fmt"
    "reflect"
)
func main() {
    var x float64 = 3.4
    fmt.Println("type:", reflect.TypeOf(x))
}

运行上述代码,输出结果如下所示:

type: float64

Type 和 Value 都包含了大量的方法,其中第一个有用的方法应该是 Kind,这个方法返回该类型的具体信息:Uint、Float64 等。Value 类型还包含了一系列类型方法,比如 Int(),用于返回对应的值。查看以下示例:

var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("type:", v.Type())
fmt.Println("kind is float64:", v.Kind() == reflect.Float64)
fmt.Println("value:", v.Float())

结果为:

type: float64
kind is float64: true
value: 3.4

2) 获取值类型

类型 Type 中有一个成员函数 CanSet(),其返回值为 bool 类型。如果你在注意到这个函数之前就直接设置了值,很有可能会收到一些看起来像异常的错误处理消息。

可能很多人会置疑为什么要有这么个奇怪的函数,可以设置所有的域不是很好吗?这里先解释一下这个函数存在的原因。

我们在之前的学习中提到过 Go语言中所有的类型都是值类型,即这些变量在传递给函数的时候将发生一次复制。基于这个原则,我们再次看一下下面的语句:

var x float64 = 3.4
v := reflect.ValueOf(x)
v.Set(4.1)

最后一条语句试图修改 v 的内容。是否可以成功地将 x 的值改为 4.1 呢?先要理清 v 和 x 的关系。在调用 ValueOf() 的地方,需要注意到 x 将会产生一个副本,因此 ValueOf() 内部对 x 的操作其实都是对着 x 的一个副本。

假如 v 允许调用 Set(),那么我们也可以想象出,被修改的将是这个 x 的副本,而不是 x 本身。如果允许这样的行为,那么执行结果将会非常困惑。调用明明成功了,为什么 x 的值还是原来的呢?为了解决这个问题 Go语言,引入了可设属性这个概念(Settability)。如果 CanSet() 返回 false,表示你不应该调用 Set() 和 SetXxx() 方法,否则会收到这样的错误:

panic: reflect.Value.SetFloat using unaddressable value

现在我们知道,有些场景下不能使用反射修改值,那么到底什么情况下可以修改的呢?其实这还是跟传值的道理类似。我们知道,直接传递一个 float 到函数时,函数不能对外部的这个 float 变量有任何影响,要想有影响的话,可以传入该 float 变量的指针。下面的示例小幅修改了之前的例子,成功地用反射的方式修改了变量 x 的值:

var x float64 = 3.4
p := reflect.ValueOf(&x) // 注意:得到X的地址
fmt.Println("type of p:", p.Type())
fmt.Println("settability of p:" , p.CanSet())
v := p.Elem()
fmt.Println("settability of v:" , v.CanSet())
v.SetFloat(7.1)
fmt.Println(v.Interface())
fmt.Println(x)

对结构的反射操作

之前讨论的都是简单类型的反射操作,现在我们讨论一下结构的反射操作。下面的示例演示了如何获取一个结构中所有成员的值:

type T struct {
    A int
    B string
}
t := T{203, "mh203"}
s := reflect.ValueOf(&t).Elem()
typeOfT := s.Type()
for i := 0; i < s.NumField(); i++ {
    f := s.Field(i)
    fmt.Printf("%d: %s %s = %v\n", i,
        typeOfT.Field(i).Name, f.Type(), f.Interface())
}

以上例子的输出为:

0: A int = 203
1: B string = mh203

可以看出,对于结构的反射操作并没有根本上的不同,只是用了 Field() 方法来按索引获取对应的成员。当然,在试图修改成员的值时,也需要注意可赋值属性。

Go语言反射规则浅析

前面讲解了 Value 和 Type 的基本概念,本节重点讲解反射对象 Value、Type 和类型实例之间的相互转化。实例、Value、Type 三者之间的转换关系如下图所示。

技术图片

图:反射对象关系

反射 API

反射 API 的分类总结如下。

1) 从实例到 Value

通过实例获取 Value 对象,直接使用 reflect.ValueOf() 函数。例如:

func ValueOf(i interface {}) Value

2) 从实例到 Type

通过实例获取反射对象的 Type,直接使用 reflect.TypeOf() 函数。例如:

func TypeOf(i interface{}) Type

3) 从 Type 到 Value

Type 里面只有类型信息,所以直接从一个 Type 接口变量里面是无法获得实例的 Value 的,但可以通过该 Type 构建一个新实例的 Value。reflect 包提供了两种方法,示例如下:

//New 返回的是一个 Value,该 Value 的 type 为 PtrTo(typ),即 Value 的 Type 是指定 typ 的指针类型
func New(typ Type) Value
//Zero 返回的是一个 typ 类型的零佳,注意返回的 Value 不能寻址,位不可改变
func Zero(typ Type) Value

如果知道一个类型值的底层存放地址,则还有一个函数是可以依据 type 和该地址值恢复出 Value 的。例如:

func NewAt(typ Type, p unsafe.Pointer) Value

4) 从 Value 到 Type

从反射对象 Value 到 Type 可以直接调用 Value 的方法,因为 Value 内部存放着到 Type 类型的指针。例如:

func (v Value) Type() Type

5) 从 Value 到实例

Value 本身就包含类型和值信息,reflect 提供了丰富的方法来实现从 Value 到实例的转换。例如:

//该方法最通用,用来将 Value 转换为空接口,该空接口内部存放具体类型实例
//可以使用接口类型查询去还原为具体的类型
func (v Value) Interface() (i interface{})

//Value 自身也提供丰富的方法,直接将 Value 转换为简单类型实例,如果类型不匹配,则直接引起 panic
func (v Value) Bool () bool
func (v Value) Float() float64
func (v Value) Int() int64
func (v Value) Uint() uint64

6) 从 Value 的指针到值

从一个指针类型的 Value 获得值类型 Value 有两种方法,示例如下。

//如果 v 类型是接口,则 Elem() 返回接口绑定的实例的 Value,如采 v 类型是指针,则返回指针值的 Value,否则引起 panic
func (v Value) Elem() Value
//如果 v 是指针,则返回指针值的 Value,否则返回 v 自身,该函数不会引起 panic
func Indirect(v Value) Value

7) Type 指针和值的相互转换

指针类型 Type 到值类型 Type。例如:

//t 必须是 Array、Chan、Map、Ptr、Slice,否则会引起 panic
//Elem 返回的是其内部元素的 Type
t.Elem() Type

值类型 Type 到指针类型 Type。例如:

//PtrTo 返回的是指向 t 的指针型 Type
func PtrTo(t Type) Type

8) Value 值的可修改性

Value 值的修改涉及如下两个方法:

//通过 CanSet 判断是否能修改
func (v Value ) CanSet() bool
//通过 Set 进行修改
func (v Value ) Set(x Value)

Value 值在什么情况下可以修改?我们知道实例对象传递给接口的是一个完全的值拷贝,如果调用反射的方法 reflect.ValueOf() 传进去的是一个值类型变量, 则获得的 Value 实际上是原对象的一个副本,这个 Value 是无论如何也不能被修改的。

如果传进去的是一个指针,虽然接口内部转换的也是指针的副本,但通过指针还是可以访问到最原始的对象,所以此种情况获得的 Value 是可以修改的。下面来看一个简单的示例。

package main
import (
    "fmt"
    "reflect"
)
type User struct {
    id int
    Name string
    Age int
}
func main() {
    u := User{Id: 1, Name:"andes", Age: 20}
    va := reflect.ValueOf(u)
    vb := reflect.ValueOf(&u)
    //值类型是可修改的
    fmt.Println(va.CanSet(), va.FieldByName("Name").CanSet()) //false false
    //指针类型是可修改的
    fmt.Println(vb.CanSet(), vb.Elem().FieldByName("Name").CanSet()) //false     false
    fmt.Printf("%v\n", vb)
    name :="shine"
    vc := reflect.ValueOf(name)
    //通过 Set 函数修改变量的值
    vb.Elem().FieldByName("Name").Set(vc)
    fmt.Printf("%v\n", vb)
}

运行结果:

false false
false true
&{1 andes 20)
&{1 shine 20)

这里归纳出了反射的三条定律:

  • 反射可以从接口值得到反射对象。
  • 反射可以从反射对象获得接口值。
  • 若要修改一个反射对象,则其值必须可以修改。

Go语言reflect.TypeOf()和reflect.Type(通过反射获取类型信息)

在 Go语言中通过调用 reflect.TypeOf 函数,我们可以从一个任何非接口类型的值创建一个 reflect.Type 值。reflect.Type 值表示着此非接口值的类型。通过此值,我们可以得到很多此非接口类型的信息。当然,我们也可以将一个接口值传递给一个 reflect.TypeOf 函数调用,但是此调用将返回一个表示着此接口值的动态类型的 reflect.Type 值。

实际上,reflect.TypeOf 函数的唯一参数的类型为 interface{},reflect.TypeOf 函数将总是返回一个表示着此唯一接口参数值的动态类型的 reflect.Type 值。

那如何得到一个表示着某个接口类型的 reflect.Type 值呢?我们必须通过下面将要介绍的一些间接途径来达到这一目的。

类型 reflect.Type 为一个接口类型,它指定了若干方法(https://golang.google.cn/pkg/reflect/#Type)。 通过这些方法,我们能够观察到一个 reflect.Type 值所表示的 Go类型的各种信息。这些方法中的有的适用于所有种类(https://golang.google.cn/pkg/reflect/#Kind)的类型,有的只适用于一种或几种类型。通过不合适的 reflect.Type 属主值调用某个方法将在运行时产生一个恐慌。

使用 reflect.TypeOf() 函数可以获得任意值的类型对象(reflect.Type),程序通过类型对象可以访问任意值的类型信息。下面通过例子来理解获取类型对象的过程:

package main
import (
    "fmt"
    "reflect"
)
func main() {
    var a int
    typeOfA := reflect.TypeOf(a)
    fmt.Println(typeOfA.Name(), typeOfA.Kind())
}

代码输出如下:

int  int

代码说明如下:
第 10 行,定义一个 int 类型的变量。
第 12 行,通过 reflect.TypeOf() 取得变量 a 的类型对象 typeOfA,类型为 reflect.Type()。
第 14 行中,通过 typeOfA 类型对象的成员函数,可以分别获取到 typeOfA 变量的类型名为 int,种类(Kind)为 int。

理解反射的类型(Type)与种类(Kind)

在使用反射时,需要首先理解类型(Type)和种类(Kind)的区别。编程中,使用最多的是类型,但在反射中,当需要区分一个大品种的类型时,就会用到种类(Kind)。例如,需要统一判断类型中的指针时,使用种类(Kind)信息就较为方便。

1) 反射种类(Kind)的定义

Go 程序中的类型(Type)指的是系统原生数据类型,如 int、string、bool、float32 等类型,以及使用 type 关键字定义的类型,这些类型的名称就是其类型本身的名称。例如使用 type A struct{} 定义结构体时,A 就是 struct{} 的类型。

种类(Kind)指的是对象归属的品种,在 reflect 包中有如下定义:

type Kind uint
const (
    Invalid Kind = iota  // 非法类型
    Bool                 // 布尔型
    Int                  // 有符号整型
    Int8                 // 有符号8位整型
    Int16                // 有符号16位整型
    Int32                // 有符号32位整型
    Int64                // 有符号64位整型
    Uint                 // 无符号整型
    Uint8                // 无符号8位整型
    Uint16               // 无符号16位整型
    Uint32               // 无符号32位整型
    Uint64               // 无符号64位整型
    Uintptr              // 指针
    Float32              // 单精度浮点数
    Float64              // 双精度浮点数
    Complex64            // 64位复数类型
    Complex128           // 128位复数类型
    Array                // 数组
    Chan                 // 通道
    Func                 // 函数
    Interface            // 接口
    Map                  // 映射
    Ptr                  // 指针
    Slice                // 切片
    String               // 字符串
    Struct               // 结构体
    UnsafePointer        // 底层指针
)

Map、Slice、Chan 属于引用类型,使用起来类似于指针,但是在种类常量定义中仍然属于独立的种类,不属于 Ptr。

type A struct{} 定义的结构体属于 Struct 种类,*A 属于 Ptr。

2) 从类型对象中获取类型名称和种类的例子

Go语言中的类型名称对应的反射获取方法是 reflect.Type 中的 Name() 方法,返回表示类型名称的字符串。

类型归属的种类(Kind)使用的是 reflect.Type 中的 Kind() 方法,返回 reflect.Kind 类型的常量。

下面的代码中会对常量和结构体进行类型信息获取。

package main
import (
    "fmt"
    "reflect"
)
// 定义一个Enum类型
type Enum int
const (
    Zero Enum = 0
)
func main() {
    // 声明一个空结构体
    type cat struct {
    }
    // 获取结构体实例的反射类型对象
    typeOfCat := reflect.TypeOf(cat{})
    // 显示反射类型对象的名称和种类
    fmt.Println(typeOfCat.Name(), typeOfCat.Kind())
    // 获取Zero常量的反射类型对象
    typeOfA := reflect.TypeOf(Zero)
    // 显示反射类型对象的名称和种类
    fmt.Println(typeOfA.Name(), typeOfA.Kind())
}

代码输出如下:

cat struct
Enum int

代码说明如下:
第 18 行,声明结构体类型 cat。
第 22 行,将 cat 实例化,并且使用 reflect.TypeOf() 获取被实例化后的 cat 的反射类型对象。
第 25 行,输出cat的类型名称和种类,类型名称就是 cat,而 cat 属于一种结构体种类,因此种类为 struct。
第 28 行,Zero 是一个 Enum 类型的常量。这个 Enum 类型在第 9 行声明,第 12 行声明了常量。如没有常量也不能创建实例,通过 reflect.TypeOf() 直接获取反射类型对象。
第 31 行,输出 Zero 对应的类型对象的类型名和种类。

Go语言reflect.Elem()——通过反射获取指针指向的元素类型

Go语言程序中对指针获取反射对象时,可以通过 reflect.Elem() 方法获取这个指针指向的元素类型。这个获取过程被称为取元素,等效于对指针类型变量做了一个 * 操作,代码如下:

package main
import (
    "fmt"
    "reflect"
)
func main() {
    // 声明一个空结构体
    type cat struct {
    }
    // 创建cat的实例
    ins := &cat{}
    // 获取结构体实例的反射类型对象
    typeOfCat := reflect.TypeOf(ins)
    // 显示反射类型对象的名称和种类
    fmt.Printf("name:'%v' kind:'%v'\n",typeOfCat.Name(), typeOfCat.Kind())
    // 取类型的元素
    typeOfCat = typeOfCat.Elem()
    // 显示反射类型对象的名称和种类
    fmt.Printf("element name: '%v', element kind: '%v'\n", typeOfCat.Name(), typeOfCat.Kind())
}

代码输出如下:

name: ''  kind: 'ptr'
element name: 'cat', element kind: 'struct'

代码说明如下:
第 15 行,创建了cat结构体的实例,ins 是一个 cat 类型的指针变量。
第 18 行,对指针变量获取反射类型信息。
第 21 行,输出指针变量的类型名称和种类。Go 语言的反射中对所有指针变量的种类都是 Ptr,但注意,指针变量的类型名称是空,不是
cat。
第 24 行,取指针类型的元素类型,也就是 cat 类型。这个操作不可逆,不可以通过一个非指针类型获取它的指针类型。
第 27 行,输出指针变量指向元素的类型名称和种类,得到了 cat 的类型名称(cat)和种类(struct)。

Go语言通过反射获取结构体的成员类型

任意值通过 reflect.TypeOf() 获得反射对象信息后,如果它的类型是结构体,可以通过反射值对象(reflect.Type)的 NumField() 和 Field() 方法获得结构体成员的详细信息。与成员获取相关的 reflect.Type 的方法如下表所示。

技术图片

结构体字段类型

reflect.Type 的 Field() 方法返回 StructField 结构,这个结构描述结构体的成员信息,通过这个信息可以获取成员与结构体的关系,如偏移、索引、是否为匿名字段、结构体标签(Struct Tag)等,而且还可以通过 StructField 的 Type 字段进一步获取结构体成员的类型信息。StructField 的结构如下:

type StructField struct {
    Name string          // 字段名
    PkgPath string       // 字段路径
    Type      Type       // 字段反射类型对象
    Tag       StructTag  // 字段的结构体标签
    Offset    uintptr    // 字段在结构体中的相对偏移
    Index     []int      // Type.FieldByIndex中的返回的索引值
    Anonymous bool       // 是否为匿名字段
}

字段说明如下。

  • Name:为字段名称。
  • PkgPath:字段在结构体中的路径。
  • Type:字段本身的反射类型对象,类型为 reflect.Type,可以进一步获取字段的类型信息。
  • Tag:结构体标签,为结构体字段标签的额外信息,可以单独提取。
  • Index:FieldByIndex 中的索引顺序。
  • Anonymous:表示该字段是否为匿名字段。

获取成员反射信息

下面代码中,实例化一个结构体并遍历其结构体成员,再通过 reflect.Type 的 FieldByName() 方法查找结构体中指定名称的字段,直接获取其类型信息。

反射访问结构体成员类型及信息:

package main
import (
    "fmt"
    "reflect"
)
func main() {
    // 声明一个空结构体
    type cat struct {
        Name string
        // 带有结构体tag的字段
        Type int `json:"type" id:"100"`
    }
    // 创建cat的实例
    ins := cat{Name: "mimi", Type: 1}
    // 获取结构体实例的反射类型对象
    typeOfCat := reflect.TypeOf(ins)
    // 遍历结构体所有成员
    for i := 0; i < typeOfCat.NumField(); i++ {
        // 获取每个成员的结构体字段类型
        fieldType := typeOfCat.Field(i)
        // 输出成员名和tag
        fmt.Printf("name: %v  tag: '%v'\n", fieldType.Name, fieldType.Tag)
    }
    // 通过字段名, 找到字段类型信息
    if catType, ok := typeOfCat.FieldByName("Type"); ok {
        // 从tag中取出需要的tag
        fmt.Println(catType.Tag.Get("json"), catType.Tag.Get("id"))
    }
}

代码输出如下:

name: Name  tag: ''
name: Type  tag: 'json:"type" id:"100"'
type 100

代码说明如下:
第 11 行,声明了带有两个成员的 cat 结构体。
第 15 行,Type 是 cat 的一个成员,这个成员类型后面带有一个以`开始和结尾的字符串。这个字符串在 Go 语言中被称为 Tag(标签)。一般用于给字段添加自定义信息,方便其他模块根据信息进行不同功能的处理。
第 19 行,创建 cat 实例,并对两个字段赋值。结构体标签属于类型信息,无须且不能赋值。
第 22 行,获取实例的反射类型对象。
第 25 行,使用 reflect.Type 类型的 NumField() 方法获得一个结构体类型共有多少个字段。如果类型不是结构体,将会触发宕机错误。
第 28 行,reflect.Type 中的 Field() 方法和 NumField 一般都是配对使用,用来实现结构体成员的遍历操作。
第 31 行,使用 reflect.Type 的 Field() 方法返回的结构不再是 reflect.Type 而是StructField 结构体。
第 35 行,使用 reflect.Type 的 FieldByName() 根据字段名查找结构体字段信息,cat Type 表示返回的结构体字段信息,类型为 StructField,ok 表示是否找到结构体字段的信息。
第 38 行中,使用 StructField 中 Tag 的 Get() 方法,根据 Tag 中的名字进行信息获取。

Go语言结构体标签(Struct Tag)

通过 reflect.Type 获取结构体成员信息 reflect.StructField 结构中的 Tag 被称为结构体标签(Struct Tag)。结构体标签是对结构体字段的额外信息标签。

JSON、BSON 等格式进行序列化及对象关系映射(Object Relational Mapping,简称 ORM)系统都会用到结构体标签,这些系统使用标签设定字段在处理时应该具备的特殊属性和可能发生的行为。这些信息都是静态的,无须实例化结构体,可以通过反射获取到。
提示
结构体标签(Struct Tag)类似于 C# 中的特性(Attribute)。C# 允许在类、字段、方法等前面添加 Attribute,然后在反射系统中可以获取到这个属性系统。例如:

[Conditional("DEBUG")]
public static void Message(string msg)
{
    Console.WriteLine(msg);
}

结构体标签的格式

Tag 在结构体字段后方书写的格式如下:

`key1:"value1" key2:"value2"`

结构体标签由一个或多个键值对组成。键与值使用冒号分隔,值用双引号括起来。键值对之间使用一个空格分隔。

从结构体标签中获取值

StructTag 拥有一些方法,可以进行 Tag 信息的解析和提取,如下所示:

  • func(tag StructTag)Get(key string)string
  • 根据 Tag 中的键获取对应的值,例如 key1:"value1"key2:"value2" 的 Tag 中,可以传入“key1”获得“value1”。
  • func(tag StructTag)Lookup(key string)(value string,ok bool)
  • 根据 Tag 中的键,查询值是否存在。

结构体标签格式错误导致的问题

编写 Tag 时,必须严格遵守键值对的规则。结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,参见下面这个例子:

package main
import (
    "fmt"
    "reflect"
)
func main() {
    type cat struct {
        Name string
        Type int `json: "type" id:"100"`
    }
    typeOfCat := reflect.TypeOf(cat{})
    if catType, ok := typeOfCat.FieldByName("Type"); ok {
        fmt.Println(catType.Tag.Get("json"))
    }
}

代码输出空字符串,并不会输出期望的 type。

第 12 行中,在json:和"type"之间增加了一个空格。这种写法没有遵守结构体标签的规则,因此无法通过 Tag.Get 获取到正确的 json 对应的值。

这个错误在开发中非常容易被疏忽,造成难以察觉的错误。

Go语言reflect.ValueOf()和reflect.Value(通过反射获取值信息)

当我们将一个接口值传递给一个 reflect.ValueOf 函数调用时,此调用返回的是代表着此接口值的动态值的一个 reflect.Value 值。我们必须通过间接的途径获得一个代表一个接口值的 reflect.Value 值。

reflect.Value 类型有很多方法(https://golang.google.cn/pkg/reflect/)。我们可以调用这些方法来观察和操纵一个 reflect.Value 属主值表示的 Go 值。这些方法中的有些适用于所有种类类型的值,有些只适用于一种或几种类型的值。

通过不合适的 reflect.Value 属主值调用某个方法将在运行时产生一个恐慌。请阅读 reflect 代码库中各个方法的文档来获取如何正确地使用这些方法。

一个 reflect.Value 值的 CanSet 方法将返回此 reflect.Value 值代表的 Go 值是否可以被修改(可以被赋值)。如果一个 Go 值可以被修改,则我们可以调用对应的 reflect.Value 值的 Set 方法来修改此 Go 值。注意:reflect.ValueOf 函数直接返回的 reflect.Value 值都是不可修改的。

反射不仅可以获取值的类型信息,还可以动态地获取或者设置变量的值。Go语言中使用 reflect.Value 获取和设置变量的值。

使用反射值对象包装任意值

Go语言中,使用 reflect.ValueOf() 函数获得值的反射值对象(reflect.Value)。书写格式如下:

value := reflect.ValueOf(rawValue)

reflect.ValueOf 返回 reflect.Value 类型,包含有 rawValue 的值信息。reflect.Value 与原值间可以通过值包装和值获取互相转化。reflect.Value 是一些反射操作的重要类型,如反射调用函数。

从反射值对象获取被包装的值

Go语言中可以通过 reflect.Value 重新获得原始值。

1) 从反射值对象(reflect.Value)中获取值的方法

可以通过下面几种方法从反射值对象 reflect.Value 中获取原值,如下表所示。

技术图片

2) 从反射值对象(reflect.Value)中获取值的例子

下面代码中,将整型变量中的值使用 reflect.Value 获取反射值对象(reflect.Value)。再通过 reflect.Value 的 Interface() 方法获得 interface{} 类型的原值,通过 int 类型对应的 reflect.Value 的 Int() 方法获得整型值。

package main
import (
    "fmt"
    "reflect"
)
func main() {
    // 声明整型变量a并赋初值
    var a int = 1024
    // 获取变量a的反射值对象
    valueOfA := reflect.ValueOf(a)
    // 获取interface{}类型的值, 通过类型断言转换
    var getA int = valueOfA.Interface().(int)
    // 获取64位的值, 强制类型转换为int类型
    var getA2 int = int(valueOfA.Int())
    fmt.Println(getA, getA2)
}

代码输出如下:

1024 1024

代码说明如下:
第 11 行,声明一个变量,类型为 int,设置初值为 1024。
第 14 行,获取变量 a 的反射值对象,类型为 reflect.Value,这个过程和 reflect.TypeOf() 类似。
第 17 行,将 valueOfA 反射值对象以 interface{} 类型取出,通过类型断言转换为 int 类型并赋值给 getA。
第 20 行,将 valueOfA 反射值对象通过 Int 方法,以 int64 类型取出,通过强制类型转换,转换为原本的 int 类型。

示例:使用reflect.Type显示一个类型的方法集

本节通过示例来演示如何使用 reflect.Type 来打印任意值的类型和枚举它的方法:

// Print prints the method set of the value x.
func Print(x interface{}) {
    v := reflect.ValueOf(x)
    t := v.Type()
    fmt.Printf("type %s\n", t)
    for i := 0; i < v.NumMethod(); i++ {
        methType := v.Method(i).Type()
        fmt.Printf("func (%s) %s%s\n", t, t.Method(i).Name,
        strings.TrimPrefix(methType.String(), "func"))
    }
}

reflect.Type 和 reflect.Value 都提供了一个 Method 方法。每次 t.Method(i) 调用将一个 reflect.Method 的实例,对应一个用于描述一个方法的名称和类型的结构体。每次 v.Method(i) 方法调用都返回一个 reflect.Value 以表示对应的值,也就是一个方法是帮到它的接收者的。

使用 reflect.Value.Call 方法,将可以调用一个 Func 类型的 Value,但是这个例子中只用到了它的类型。这是属于 time.Duration 和 *strings.Replacer 两个类型的方法:

methods.Print(time.Hour)
// Output:
// type time.Duration
// func (time.Duration) Hours() float64
// func (time.Duration) Minutes() float64
// func (time.Duration) Nanoseconds() int64
// func (time.Duration) Seconds() float64
// func (time.Duration) String() string
methods.Print(new(strings.Replacer))
// Output:
// type *strings.Replacer
// func (*strings.Replacer) Replace(string) string
// func (*strings.Replacer) WriteString(io.Writer, string) (int, error)
\`

Go语言通过反射访问结构体成员的值

反射值对象(reflect.Value)提供对结构体访问的方法,通过这些方法可以完成对结构体任意值的访问,如下表所示。

技术图片

下面代码构造一个结构体包含不同类型的成员。通过 reflect.Value 提供的成员访问函数,可以获得结构体值的各种数据。

反射访问结构体成员值:

package main
import (
    "fmt"
    "reflect"
)
// 定义结构体
type dummy struct {
    a int
    b string
    // 嵌入字段
    float32
    bool
    next *dummy
}
func main() {
    // 值包装结构体
    d := reflect.ValueOf(dummy{
            next: &dummy{},
    })
    // 获取字段数量
    fmt.Println("NumField", d.NumField())
    // 获取索引为2的字段(float32字段)
    floatField := d.Field(2)
    // 输出字段类型
    fmt.Println("Field", floatField.Type())
    // 根据名字查找字段
    fmt.Println("FieldByName(\"b\").Type", d.FieldByName("b").Type())
    // 根据索引查找值中, next字段的int字段的值
    fmt.Println("FieldByIndex([]int{4, 0}).Type()", d.FieldByIndex([]int{4, 0}).Type())
}

代码说明如下:
第 9 行,定义结构体,结构体的每个字段的类型都不一样。
第 24 行,实例化结构体并包装为 reflect.Value 类型,成员中包含一个 *dummy 的实例。
第 29 行,获取结构体的字段数量。
第 32 和 35 行,获取索引为2的字段值(float32 字段),并且打印类型。
第 38 行,根据b字符串,查找到 b 字段的类型。
第 41 行,[]int{4,0} 中的 4 表示,在 dummy 结构中索引值为 4 的成员,也就是 next。next 的类型为 dummy,也是一个结构体,因此使用 []int{4,0} 中的 0 继续在 next 值的基础上索引,结构为 dummy 中索引值为 0 的 a 字段,类型为 int。

代码输出如下:

NumField 5
Field float32
FieldByName("b").Type string
FieldByIndex([]int{4, 0}).Type() int

Go语言IsNil()和IsValid()——判断反射值的空和有效性

反射值对象(reflect.Value)提供一系列方法进行零值和空判定,如下表所示。

技术图片

下面的例子将会对各种方式的空指针进行 IsNil() 和 IsValid() 的返回值判定检测。同时对结构体成员及方法查找 map 键值对的返回值进行 IsValid() 判定,参考下面的代码。

反射值对象的零值和有效性判断:

package main
import (
    "fmt"
    "reflect"
)
func main() {
    // *int的空指针
    var a *int
    fmt.Println("var a *int:", reflect.ValueOf(a).IsNil())
    // nil值
    fmt.Println("nil:", reflect.ValueOf(nil).IsValid())
    // *int类型的空指针
    fmt.Println("(*int)(nil):", reflect.ValueOf((*int)(nil)).Elem().IsValid())
    // 实例化一个结构体
    s := struct{}{}
    // 尝试从结构体中查找一个不存在的字段
    fmt.Println("不存在的结构体成员:", reflect.ValueOf(s).FieldByName("").IsValid())
    // 尝试从结构体中查找一个不存在的方法
    fmt.Println("不存在的结构体方法:", reflect.ValueOf(s).MethodByName("").IsValid())
    // 实例化一个map
    m := map[int]int{}
    // 尝试从map中查找一个不存在的键
    fmt.Println("不存在的键:", reflect.ValueOf(m).MapIndex(reflect.ValueOf(3)).IsValid())
}

代码输出如下:

var a *int: true
nil: false
(*int)(nil): false
不存在的结构体成员: false
不存在的结构体方法: false
不存在的键: false

代码说明如下:
第 11 行,声明一个 int 类型的指针,初始值为 nil。
第 12 行,将变量 a 包装为 reflect.Value 并且判断是否为空,此时变量 a 为空指针,因此返回 true。
第 15 行,对 nil 进行 IsValid() 判定(有效性判定),返回 false。
第 18 行,(
int)(nil) 的含义是将 nil 转换为 int,也就是int 类型的空指针。此行将 nil 转换为 int 类型,并取指针指向元素。由于 nil 不指向任何元素,int 类型的 nil 也不能指向任何元素,值不是有效的。因此这个反射值使用 Isvalid() 判断时返回 false。
第 21 行,实例化一个结构体。
第 24 行,通过 FieldByName 查找 s 结构体中一个空字符串的成员,如成员不存在,IsValid() 返回 false。
第 27 行,通过 MethodByName 查找 s 结构体中一个空字符串的方法,如方法不存在,IsValid() 返回 false。
第 30 行,实例化一个 map,这种写法与 make 方式创建的 map 等效。
第 33 行,MapIndex() 方法能根据给定的 reflect.Value 类型的值查找 map,并且返回查找到的结果。

IsNil() 常被用于判断指针是否为空;IsValid() 常被用于判定返回值是否有效。

Go语言通过反射修改变量的值

Go语言中类似 x、x.f[1] 和 *p 形式的表达式都可以表示变量,但是其它如 x + 1 和 f(2) 则不是变量。一个变量就是一个可寻址的内存空间,里面存储了一个值,并且存储的值可以通过内存地址来更新。

对于 reflect.Values 也有类似的区别。有一些 reflect.Values 是可取地址的;其它一些则不可以。考虑以下的声明语句:

x := 2 // value type variable?
a := reflect.ValueOf(2) // 2 int no
b := reflect.ValueOf(x) // 2 int no
c := reflect.ValueOf(&x) // &x *int no
d := c.Elem() // 2 int yes (x)

其中 a 对应的变量则不可取地址。因为 a 中的值仅仅是整数 2 的拷贝副本。b 中的值也同样不可取地址。c 中的值还是不可取地址,它只是一个指针 &x 的拷贝。实际上,所有通过 reflect.ValueOf(x) 返回的 reflect.Value 都是不可取地址的。但是对于 d,它是 c 的解引用方式生成的,指向另一个变量,因此是可取地址的。我们可以通过调用 reflect.ValueOf(&x).Elem(),来获取任意变量x对应的可取地址的 Value。

我们可以通过调用 reflect.Value 的 CanAddr 方法来判断其是否可以被取地址:

fmt.Println(a.CanAddr()) // "false"
fmt.Println(b.CanAddr()) // "false"
fmt.Println(c.CanAddr()) // "false"
fmt.Println(d.CanAddr()) // "true"

每当我们通过指针间接地获取的 reflect.Value 都是可取地址的,即使开始的是一个不可取地址的 Value。在反射机制中,所有关于是否支持取地址的规则都是类似的。例如,slice 的索引表达式 e[i]将隐式地包含一个指针,它就是可取地址的,即使开始的e表达式不支持也没有关系。

以此类推,reflect.ValueOf(e).Index(i) 对于的值也是可取地址的,即使原始的 reflect.ValueOf(e) 不支持也没有关系。

使用 reflect.Value 对包装的值进行修改时,需要遵循一些规则。如果没有按照规则进行代码设计和编写,轻则无法修改对象值,重则程序在运行时会发生宕机。

判定及获取元素的相关方法

使用 reflect.Value 取元素、取地址及修改值的属性方法请参考下表。

技术图片

值修改相关方法

使用 reflect.Value 修改值的相关方法如下表所示。

技术图片

以上方法,在 reflect.Value 的 CanSet 返回 false 仍然修改值时会发生宕机。

在已知值的类型时,应尽量使用值对应类型的反射设置值。

值可修改条件之一:可被寻址

通过反射修改变量值的前提条件之一:这个值必须可以被寻址。简单地说就是这个变量必须能被修改。示例代码如下:

package main
import (
    "reflect"
)
func main() {
    // 声明整型变量a并赋初值
    var a int = 1024
    // 获取变量a的反射值对象
    valueOfA := reflect.ValueOf(a)
    // 尝试将a修改为1(此处会发生崩溃)
    valueOfA.SetInt(1)
}

程序运行崩溃,打印错误:

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

报错意思是:SetInt 正在使用一个不能被寻址的值。从 reflect.ValueOf 传入的是 a 的值,而不是 a 的地址,这个 reflect.Value 当然是不能被寻址的。将代码修改一下,重新运行:

package main
import (
    "fmt"
    "reflect"
)
func main() {
    // 声明整型变量a并赋初值
    var a int = 1024
    // 获取变量a的反射值对象(a的地址)
    valueOfA := reflect.ValueOf(&a)
    // 取出a地址的元素(a的值)
    valueOfA = valueOfA.Elem()
    // 修改a的值为1
    valueOfA.SetInt(1)
    // 打印a的值
    fmt.Println(valueOfA.Int())
}

代码输出如下:

1

下面是对代码的分析:
第 14 行中,将变量 a 取值后传给 reflect.ValueOf()。此时 reflect.ValueOf() 返回的 valueOfA 持有变量 a 的地址。
第 17 行中,使用 reflect.Value 类型的 Elem() 方法获取 a 地址的元素,也就是 a 的值。reflect.Value 的 Elem() 方法返回的值类型也是 reflect.Value。
第 20 行,此时 valueOfA 表示的是 a 的值且可以寻址。使用 SetInt() 方法设置值时不再发生崩溃。
第 23 行,正确打印修改的值。

提示:当 reflect.Value 不可寻址时,使用 Addr() 方法也是无法取到值的地址的,同时会发生宕机。虽然说 reflect.Value 的 Addr() 方法类似于语言层的&操作;Elem() 方法类似于语言层的*操作,但并不代表这些方法与语言层操作等效。

值可修改条件之一:被导出

结构体成员中,如果字段没有被导出,即便不使用反射也可以被访问,但不能通过反射修改,代码如下:

package main
import (
    "reflect"
)
func main() {
    type dog struct {
            legCount int
    }
    // 获取dog实例的反射值对象
    valueOfDog := reflect.ValueOf(dog{})
    // 获取legCount字段的值
    vLegCount := valueOfDog.FieldByName("legCount")
    // 尝试设置legCount的值(这里会发生崩溃)
    vLegCount.SetInt(4)
}

程序发生崩溃,报错:

panic: reflect: reflect.Value.SetInt using value obtained using unexported field

报错的意思是:SetInt() 使用的值来自于一个未导出的字段。

为了能修改这个值,需要将该字段导出。将 dog 中的 legCount 的成员首字母大写,导出 LegCount 让反射可以访问,修改后的代码如下:

type dog struct {
    LegCount int
}

然后根据字段名获取字段的值时,将字符串的字段首字母大写,修改后的代码如下:

vLegCount := valueOfDog.FieldByName("LegCount")

再次运行程序,发现仍然报错:

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

这个错误表示第 13 行构造的 valueOfDog 这个结构体实例不能被寻址,因此其字段也不能被修改。修改代码,取结构体的指针,再通过 reflect.Value 的 Elem() 方法取到值的反射值对象。修改后的完整代码如下:

package main
import (
    "reflect"
    "fmt"
)
func main() {
    type dog struct {
            LegCount int
    }
    // 获取dog实例地址的反射值对象
    valueOfDog := reflect.ValueOf(&dog{})
    // 取出dog实例地址的元素
    valueOfDog = valueOfDog.Elem()
    // 获取legCount字段的值
    vLegCount := valueOfDog.FieldByName("LegCount")
    // 尝试设置legCount的值(这里会发生崩溃)
    vLegCount.SetInt(4)
    fmt.Println(vLegCount.Int())
}

代码输出如下:

4

代码说明如下:
第 11 行,将 LegCount 首字母大写导出该字段。
第 14 行,获取 dog 实例指针的反射值对象。
第 17 行,取 dog 实例的指针元素,也就是 dog 的实例。
第 20 行,取 dog 结构体中 LegCount 字段的成员值。
第 23 行,修改该成员值。
第 25 行,打印该成员值。

值的修改从表面意义上叫可寻址,换一种说法就是值必须“可被设置”。那么,想修改变量值,一般的步骤是:

  • 取这个变量的地址或者这个变量所在的结构体已经是指针类型。
  • 使用 reflect.ValueOf 进行值包装。
  • 通过 Value.Elem() 获得指针值指向的元素值对象(Value),因为值对象(Value)内部对象为指针时,使用 set 设置时会报出宕机错误。
  • 使用 Value.Set 设置值。

示例:获取结构体字段标识

在本节,我们将看到如何通过反射机制类获取成员标签。

对于一个 web 服务,大部分 HTTP 处理函数要做的第一件事情就是展开请求中的参数到本地变量中。我们定义了一个工具函数,叫 params.Unpack,通过使用结构体成员标签机制来让 HTTP 处理函数解析请求参数更方便。

首先,我们看看如何使用它。下面的 search 函数是一个 HTTP 请求处理函数。它定义了一个匿名结构体类型的变量,用结构体的每个成员表示 HTTP 请求的参数。

其中结构体成员标签指明了对于请求参数的名字,为了减少 URL 的长度这些参数名通常都是神秘的缩略词。Unpack 将请求参数填充到合适的结构体成员中,这样我们可以方便地通过合适的类型类来访问这些参数。

import "gopl.io/ch12/params"
// 搜索实现/search url端点。
func search(resp http.ResponseWriter, req *http.Request) {
    var data struct {
        Labels []string `http:"l"`
        MaxResults int `http:"max"`
        Exact bool `http:"x"`
    }
    data.MaxResults = 10 // set default
    if err := params.Unpack(req, &data); err != nil {
        http.Error(resp, err.Error(), http.StatusBadRequest) // 400
        return
    }
    // ...处理程序的其余部分...
    fmt.Fprintf(resp, "Search: %+v\n", data)
}

下面的 Unpack 函数主要完成三件事情。第一,它调用 req.ParseForm() 来解析 HTTP 请求。然后,req.Form 将包含所有的请求参数,不管 HTTP 客户端使用的是 GET 还是 POST 请求方法。

下一步,Unpack 函数将构建每个结构体成员有效参数名字到成员变量的映射。如果结构体成员有成员标签的话,有效参数名字可能和实际的成员名字不相同。reflect.Type 的 Field 方法将返回一个 reflect.StructField,里面含有每个成员的名字、类型和可选的成员标签等信息。

其中成员标签信息对应 reflect.StructTag 类型的字符串,并且提供了 Get 方法用于解析和根据特定 key 提取的子串,例如这里的 http:"..." 形式的子串。

// unpack填充ptr指向的结构的字段
// 从请求中的HTTP请求参数。
func Unpack(req *http.Request, ptr interface{}) error {
    if err := req.ParseForm(); err != nil {
        return err
    }
    // 构建由有效名称键控的字段的映射。
    fields := make(map[string]reflect.Value)
    v := reflect.ValueOf(ptr).Elem() // 结构变量
    for i := 0; i < v.NumField(); i++ {
        fieldInfo := v.Type().Field(i) // 反射.StructField
        tag := fieldInfo.Tag // 一个 reflect.StructTag
        name := tag.Get("http")
        if name == "" {
            name = strings.ToLower(fieldInfo.Name)
        }
        fields[name] = v.Field(i)
    }
    // 为请求中的每个参数更新结构字段。
    for name, values := range req.Form {
        f := fields[name]
        if !f.IsValid() {
            continue // 忽略无法识别的HTTP参数
        }
        for _, value := range values {
            if f.Kind() == reflect.Slice {
                elem := reflect.New(f.Type().Elem()).Elem()
                if err := populate(elem, value); err != nil {
                    return fmt.Errorf("%s: %v", name, err)
                }
                f.Set(reflect.Append(f, elem))
            } else {
                if err := populate(f, value); err != nil {
                    return fmt.Errorf("%s: %v", name, err)
                }
            }
        }
    }
    return nil
}

最后,Unpack 遍历 HTTP 请求的 name/valu 参数键值对,并且根据更新相应的结构体成员。回想一下,同一个名字的参数可能出现多次。如果发生这种情况,并且对应的结构体成员是一个 slice,那么就将所有的参数添加到 slice 中。其它情况,对应的成员值将被覆盖,只有最后一次出现的参数值才是起作用的。

populate 函数小心用请求的字符串类型参数值来填充单一的成员 v(或者是 slice 类型成员中的单一的元素)。目前,它仅支持字符串、有符号整数和布尔型。

func populate(v reflect.Value, value string) error {
    switch v.Kind() {
        case reflect.String:
            v.SetString(value)
        case reflect.Int:
            i, err := strconv.ParseInt(value, 10, 64)
            if err != nil {
                return err
            }
            v.SetInt(i)
        case reflect.Bool:
            b, err := strconv.ParseBool(value)
            if err != nil {
                return err
            }
            v.SetBool(b)
        default:
            return fmt.Errorf("unsupported kind %s", v.Type())
    }
    return nil
}

如果将上面的处理程序添加到一个 web 服务器,则可以产生以下的会话:

$ go build gopl.io/ch12/search
$ ./search &
$ ./fetch 'http://localhost:12345/search'
Search: {Labels:[] MaxResults:10 Exact:false}
$ ./fetch 'http://localhost:12345/search?l=golang&l=programming'
Search: {Labels:[golang programming] MaxResults:10 Exact:false}
$ ./fetch 'http://localhost:12345/search?l=golang&l=programming&max=100'
Search: {Labels:[golang programming] MaxResults:100 Exact:false}
$ ./fetch 'http://localhost:12345/search?x=true&l=golang&l=programming'
Search: {Labels:[golang programming] MaxResults:10 Exact:true}
$ ./fetch 'http://localhost:12345/search?q=hello&x=123'
x: strconv.ParseBool: parsing "123": invalid syntax
$ ./fetch 'http://localhost:12345/search?q=hello&max=lots'
max: strconv.ParseInt: parsing "lots": invalid syntax

Go语言通过类型信息创建实例

当已知 reflect.Type 时,可以动态地创建这个类型的实例,实例的类型为指针。例如 reflect.Type 的类型为 int 时,创建 int 的指针,即*int,代码如下:

package main
import (
    "fmt"
    "reflect"
)
func main() {
    var a int
    // 取变量a的反射类型对象
    typeOfA := reflect.TypeOf(a)
    // 根据反射类型对象创建类型实例
    aIns := reflect.New(typeOfA)
    // 输出Value的类型和种类
    fmt.Println(aIns.Type(), aIns.Kind())
}

代码输出如下:

*int ptr

代码说明如下:
第 13 行,获取变量 a 的反射类型对象。
第 16 行,使用 reflect.New() 函数传入变量 a 的反射类型对象,创建这个类型的实例值,值以 reflect.Value 类型返回。这步操作等效于:new(int),因此返回的是 *int 类型的实例。
第 19 行,打印 aIns 的类型为 *int,种类为指针。

Go语言通过反射调用函数

如果反射值对象(reflect.Value)中值的类型为函数时,可以通过 reflect.Value 调用该函数。使用反射调用函数时,需要将参数使用反射值对象的切片 []reflect.Value 构造后传入 Call() 方法中,调用完成时,函数的返回值通过 []reflect.Value 返回。

下面的代码声明一个加法函数,传入两个整型值,返回两个整型值的和。将函数保存到反射值对象(reflect.Value)中,然后将两个整型值构造为反射值对象的切片([]reflect.Value),使用 Call() 方法进行调用。

反射调用函数:

package main
import (
    "fmt"
    "reflect"
)
// 普通函数
func add(a, b int) int {
    return a + b
}
func main() {
    // 将函数包装为反射值对象
    funcValue := reflect.ValueOf(add)
    // 构造函数参数, 传入两个整型值
    paramList := []reflect.Value{reflect.ValueOf(10), reflect.ValueOf(20)}
    // 反射调用函数
    retList := funcValue.Call(paramList)
    // 获取第一个返回值, 取整数值
    fmt.Println(retList[0].Int())
}

代码说明如下:
第 9~12 行,定义一个普通的加法函数。
第 17 行,将 add 函数包装为反射值对象。
第 20 行,将 10 和 20 两个整型值使用 reflect.ValueOf 包装为 reflect.Value,再将反射值对象的切片 []reflect.Value 作为函数的参数。
第 23 行,使用 funcValue 函数值对象的 Call() 方法,传入参数列表 paramList 调用 add() 函数。
第 26 行,调用成功后,通过 retList[0] 取返回值的第一个参数,使用 Int 取返回值的整数值。

提示:反射调用函数的过程需要构造大量的 reflect.Value 和中间变量,对函数参数值进行逐一检查,还需要将调用参数复制到调用函数的参数内存中。调用完毕后,还需要将返回值转换为 reflect.Value,用户还需要从中取出调用值。因此,反射调用函数的性能问题尤为突出,不建议大量使用反射函数调用。

Go语言inject库:依赖注入

前面己经对反射的基本概念和相关 API 进行了讲解,本节结合一个非常著名的包 inject 展开讲解,inject 借助反射提供了对 2 种类型实体的注入:函数和结构。Go 著名的 Web 框架 martini 的依赖注入使用的就是这个包。

依赖注入和控制反转

在介绍 inject 之前先简单介绍“依赖注入”和“控制反转”的概念。正常情况下,对函数或方法的调用是调用方的主动直接行为,调用方清楚地知道被调的函数名是什么,参数有哪些类型,直接主动地调用;包括对象的初始化也是显式地直接初始化。

所谓的“控制反转”就是将这种主动行为变成间接的行为,主调方不是直接调用函数或对象,而是借助框架代码进行间接的调用和初始化,这种行为我们称为“控制反转”,控制反转可以解藕调用方和被调方。

“库”和“框架”能很好地解释“控制反转”的概念。一般情况下,使用库的程序是程序主动地调用库的功能,但使用框架的程序常常由框架驱动整个程序,在框架下写的业务代码是被框架驱动的,这种模式就是“控制反转”。

“依赖注入”是实现“控制反转”的一种方法,如果说“控制反转”是一种设计思想,那么“依赖注入”就是这种思想的一种实现,通过注入的参数或实例的方式实现控制反转。如果没有特殊说明,我们通常说的“依赖注入”和“控制反转”是一个东西。

大家可能会疑惑,为什么不直接光明正大地调用,而非要拐弯抹角地进行间接调用,控制反转的价值在哪里呢?一句话“解耦”,有了控制反转就不需要调用者将代码写死,可以让控制反转的框架代码读取配置,动态地构建对象,这一点在 Java 的 Spring 框架中体现得尤为突出。

控制反转是解决复杂问题一种方法,特别是在 Web 框架中为路由和中间件的灵活注入提供了很好的方法。但是软件开发没有银弹,当问题足够复杂时,应该考虑的是服务拆分,而不是把复杂的逻辑用一个“大盒子”装起来,看起来干净了,但也只是看起来干净,实现还是很复杂,这也是使用框架带来的副作用。

inject 实践

inject 是 Go语言依赖注入的实现,它实现了对结构(struct)和函数的依赖注入。在介绍具体实现之前,先来想一个问题,如何通过一个字符串类型的函数名调用函数。Go 没有 Java 中的 Class.forName 方法可以通过类名直接构造对象,所以这种方法是行不通的,能想到的方法就是使用 map 实现一个字符串到函数的映射,代码如下:

func fl() {
    println ("fl")
}
func f2 () {
    println ("f2")
}
funcs := make(map[string] func ())
funcs ["fl"] = fl
funcs ["f2"] = fl
funcs ["fl"]()
funcs ["f2"]()

但是这有个缺陷,就是 map 的 Value 类型被写成 func(),不同参数和返回值的类型的函数并不能通用。将 map 的 Value 定义为 interface{} 空接口类型是否能解决该问题?可以解决该问题,但需要借助类型断言或反射来实现,通过类型断言实现等于又绕回去了,反射是一种可行的办法。inject 包借助反射实现函数的注入调用,下面通过一个例子来看一下。

package main
import (
    "fmt"
    "github.com/codegangsta/inject"
)
type Sl interface{}
type S2 interface{}
func Format(name string, company S1, level S2, age int) {
    fmt.Printf("name = %s, company=%s, level=%s, age = %d!\n", name, company, level, age)
}
func main() {
    //控制实例的创建
    inj := inject.New()
    //实参注入
    inj.Map("tom")
    inj.MapTo("tencent", (*S1) (nil))
    inj.MapTo("T4", (*S2) (nil))
    inj.Map(23)
    //函数反转调用
    inj.Invoke(Format)
}

执行结果:

name=tom, cornpany=tencent, level=T4, age=23!

可见 inject 提供了一种注入参数调用函数的通用功能,inject.New() 相当于创建了一个控制实例,由其来实现对函数的注入调用。inject 包不但提供了对函数的注入,还实现了对 struct 类型的注入,看下一个示例。

package main
import (
    "fmt"
    "github.com/codegangsta/inject"
)
type S1 interface{}
type S2 interface{}
type Staff struct {
    Name string `inject`
    Company S1 `inject`
    Level S2 `inject`
    Age int `inject`
}
func main() {
    //创建被注入实例
    s := Staff {}
    //控制实例的创建
    inj := inject.New()
    //初始化注入值
    inj.Map ("tom")
    inj.MapTo ("tencent", (*S1) (nil))
    inj.MapTo ("T4",(*S2) (nil))
    inj.Map(23)
    //实现对 struct 注入
    inj.Apply (&s)
    //打印结果
    fmt.Printf ("s = %v\n", s)
}

执行结果:

s = {tom tencent T4 23}

可以看到 inject 提供了一种对结构类型的通用注入方法。至此,我们仅仅从宏观层面了解 iniect 能做什么,下面从源码实现角度来分析 inject。

inject 原理分析

inject 包只有 178 行代码(包括注释),却提供了一个完美的依赖注入实现,下面采用自顶向下的方法分析其实现原理。

入口函数 New

inject.New() 函数构建一个具体类型 injector 实例作为内部注入引擎,返回的是一个 Injector 类型的接口。这里也体现了一种面向接口的设计思想:对外暴露接口方法,对外隐藏内部实现。示例如下:

func New() Injector {
    return &injector {
        values : make(map[reflect.Type)reflect.Value),
    }
}

接口设计

下面来看一下具体的接口设计,Injector 暴露了所有方法给外部使用者,这些方法又可以归纳为两大类。第一类方法是对参数注入进行初始化,将结构类型的字段的注入和函数的参数注入统一成一套方法实现;第二类是专用注入实现,分别是生成结构对象和调用函数方法。

在代码设计上,inject 将接口的粒度拆分得很细,将多个接口组合为一个大的接口,这也符合 Go 的 Duck 类型接口设计的原则。injector 按照上述方法拆分为三个接口。示例如下:

type Injector interface {
    //抽象生成注入结构实例的接口
    Applicator
    //抽象函数调用的接口
    Invoker
    //抽象注入参数的接口
    TypeMapper
    //实现一个注入实例链, 下游的能覆盖上游的类型
    SetParent(Injector)
}

TypeMapper 接口实现对注入参数操作的汇总,包括设置和查找相关的类型和值的方法。注意:无论函数的实参,还是结构的字段,在 inject 内部,都存放在 map[reflect.Type]reflect.Value 类型的 map 里面,具体实现在后面介绍 injector 时会讲解。

type TypeMapper interface {
    //如下三个方法是设直参数
    Map(interface{}) TypeMapper
    MapTo(interface{}, interface{}) TypeMapper
    Set(reflect.Type, reflect.Value) TypeMapper
    //查找参数
    Get(reflect.Type) reflect.Value
}

Invoker 接口中 Invoke 方法是对被注入实参函数的调用:

type Invoker interface {
    Invoke (interface{}) ([]reflect.Value, error)
}

Applicator 接口中 Apply 方法实现对结构的注入:

type Applicator interface {
    Apply(interface{}) error
}

下面梳理了整个 inject 包的处理流程:

  • 通过 inject.New() 创建注入引擎,注入引擎被隐藏,返回的是 Injector 接口类型变量。
  • 调用 TypeMapper 接口(Injector 内嵌 TypeMapper)的方法注入 struct 的字段值或函数的实参值。
  • 调用 Invoker 方法执行被注入的函数,或者调用 Applicator 接口方法获得被注入后的结构实例。

内部实现

下面具体看一下 inject 内部注入引擎 injector 的实现,首先看一下 injector 的数据结构。

type injector struct {
    values map[reflect.Type]reflect.Value
    parent Injector
}

values 里面存放的可以是被注入 struct 的字段类型和值,也可以是函数实参的类型和值。注意:values 是以 reflect.Type 为 Key 的 map,如果一个结构的字段类型相同,则后面注入的参数会覆盖前面的参数,规避办法是使用 MapTo 方法,通过抽象出一个接口类型来避免被覆盖。

func (i *injector) MapTo (val interface{}, ifacePtr interface{}) TypeMapper {
    i.values[InterfaceOf(ifacePtr)] = reflect.ValueOf (val)
    return i
}

injector 里面的 parent 的作用是实现多个注入引擎,其构成了一个链。

下面重点分析 injector 对函数的注入实现。示例如下:

func (inj *injector) Invoke(f interface{}) ([]reflect.Value, error) {
    //获取函数类型的 Type
    t := reflect.TypeOf(f)
    //构造一个存放函数实参 Value 值的数纽
    var in = make([]reflect.Value, t.NumIn())
    //使用反射获取函数实参 reflect.Type,逐个去 injector 中查找注入的 Value 值
    for i := O; i < t.NumIn(); i++ {
        argType := t.In(i)
        val := inj.Get(argType)
        if !val.IsValid() {
            return nil, fmt.Errorf("Value not found for type %v", argType)
        }
        in[i] = val
    }
    //反射调用函数
    return reflect.ValueOf(f).Call(in), nil
}

inject 对函数注入调用实现很简洁,就是从 injector 里面获取函数实参,然后调用函数。

通过对 inject 包的分析,认识到其“短小精悍”、功能强大,这些实现的基础是依靠反射。但同时注意到包含反射的代码相对来说复杂难懂,虽然 inject 的实现只有短短 200 行代码,但阅读起来并不是很流畅。所以说反射是一把双刃剑,好用但代码不好读。

10. Go 语言反射

标签:有符号   映射   inf   json   nic   unsafe   script   java ee   Staff   

原文地址:https://www.cnblogs.com/kershaw/p/12077189.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!