Go语言--反射(reflect)

0 反射的概念

反射是指计算机程序在运行时(runtime)可以访问、检测和修改本身状态或行为的一种能力。通俗地将,反射就是程序能够在运行时动态地查看自己的状态,并且允许修改自身的行为。

程序在编译时,变量被转换为内存地址,变量名不会被编译器写入到可执行文件中。在运行程序时,程序无法获取自身的信息。但是,支持反射的编程语言可以在编译期将变量的反射信息,如字段名称、类型信息、结构体信息等整合到可执行文件中,并给程序提供接口访问反射信息,这样就可以在程序运行期获取变量的反射信息,并且有能力修改它们。

Go语言反射的基础是编译器和运行时把类型信息已合适的数据结构保存在可执行程序中。在Go语言中反射的相关功能由内置的 reflect包 提供。因此,我们主要是要熟悉 reflect 标准库的用法,以及简要了解Go语言的反射内部的实现原理。

Go语言的反射建立在Go类型系统基础之上,和接口有紧密的关系,在学习接口之前首先要了解Go语言的接口用法。

1 reflect 包

在Go语言的反射机制中,任何接口值都是由一个具体类型 和 具体类型的值 两部分组成。任何接口值在反射中都可以理解为由 reflect.Typereflect.Value 两部分组成,并且reflect包提供了 reflect.TypeOfreflect.ValueOf 两个函数来获取任意对象的 Type 和 Value。

reflect包源码位置:src/reflect,该目录下有两个主要的Go源文件:type.go 和 value.go。

2 反射的类型对象 — reflect.Type

反射功能由 reflect 包提供,它定义了两个重要的类型:Type 和 Value。Type 表示Go语言的一个类型,它是一个有很多方法的接口类型,这些方法可以用来识别类型及透视类型的组成部分,比如一个结构体的各个字段或者一个函数的各个参数。reflect.Type接口只有一个实现,即类型描述符,接口值中的动态类型也是类型描述符。

在Go语言的反射包里面有一个通用的描述类型公共信息的结构体 rtype(定义在:src/reflect/type.go),这个 rtype 结构体实际上和接口内部实现时的 runtime 包里面的 _type 结构体是同一个东西,只是因为包的隔离性而分开定义而已(定义在:src/runtime/type.go),都是描述类型的通用信息,同时为每一种基础类型封装了一个特定的结构。rtype 结构体的定义如下:

// rtype is the common implementation of most values.
// It is embedded in other struct types.
//
// rtype must be kept in sync with ../runtime/type.go:/^type._type. -- 通用类型
type rtype struct {
    size       uintptr
    ptrdata    uintptr // number of bytes in the type that can contain pointers
    hash       uint32  // hash of type; avoids computation in hash tables
    tflag      tflag   // extra type information flags
    align      uint8   // alignment of variable with this type
    fieldAlign uint8   // alignment of struct field with this type
    kind       uint8   // enumeration for C
    // function for comparing objects of this type
    // (ptr to object A, ptr to object B) -> ==?
    equal     func(unsafe.Pointer, unsafe.Pointer) bool
    gcdata    *byte   // garbage collection data
    str       nameOff // string form
    ptrToThis typeOff // type for pointer to this type, may be zero
}

// arrayType represents a fixed array type. -- 数组类型
type arrayType struct {
    rtype
    elem  *rtype // array element type
    slice *rtype // slice type
    len   uintptr
}

// chanType represents a channel type. -- 通道类型
type chanType struct {
    rtype
    elem *rtype  // channel element type
    dir  uintptr // channel direction (ChanDir)
}

// interfaceType represents an interface type. -- 接口类型
type interfaceType struct {
    rtype
    pkgPath name      // import path
    methods []imethod // sorted by hash
}

// mapType represents a map type.  -- 映射类型
type mapType struct {
    rtype
    key    *rtype // map key type
    elem   *rtype // map element (value) type
    bucket *rtype // internal bucket structure
    // function for hashing keys (ptr to key, seed) -> hash
    hasher     func(unsafe.Pointer, uintptr) uintptr
    keysize    uint8  // size of key slot
    valuesize  uint8  // size of value slot
    bucketsize uint16 // size of bucket
    flags      uint32
}

// ptrType represents a pointer type. -- 指针类型
type ptrType struct {
    rtype
    elem *rtype // pointer element (pointed at) type
}

// sliceType represents a slice type. -- 切片类型
type sliceType struct {
    rtype
    elem *rtype // slice element type
}

在 src/reflect/type.go 源文件中,rtype 结构体类型实现了 reflect.Type 接口。

在Go程序中,使用 reflect.TypeOf()函数可以获得任意值的类型对象(reflect.Type)。程序可以通过类型对象可以访问任意值的类型信息。reflect.TypeOf() 的函数原型如下:

// src/reflect/type.go
func TypeOf(i interface{}) Type

可以看到,TypeOf() 函数 可以接受任意类型的参数,并且把接口中的动态类型以 reflect.Type 形式返回。

示例代码:

package main

import (
    "fmt"
    "reflect"
)

func main() {
    t := reflect.TypeOf(3)
    fmt.Println(t)             //输出:int
    fmt.Println(t.String())    //输出:int
}

《代码说明》上面的 TypeOf(3) 调用把数值3赋值给 interface{} 参数。回想一下接口值的赋值,把一个具体类型的值赋值给一个接口类型变量时会发生一个隐式类型转换,转换会生成一个包含两部分内容的接口值:动态类型部分是操作数的类型(int),动态值部分是操作数的值(3)。

因为 reflect.TypeOf() 函数返回一个接口值对应的动态类型,所以它返回的总是具体类型(而不是接口类型),当然也可以让 reflect.Type 表示一个接口类型。示例代码如下:

package main

import (
    "fmt"
    "reflect"
    "io"
    "os"
)

func main() {
    var w io.Writer = os.Stdout    //声明一个io.Writer的接口类型变量w,并初始化为 os.Stdout
    fmt.Println(reflect.TypeOf(w)) //输出:*os.File
}

《代码说明》可以看到,输出结果是:*os.File,而不是:io.Writer。因为 os.Stdout 是 *os.File 类型,也就是说 reflect.TypeOf 返回的是一个具体类型,而不是接口类型本身。

2.1 反射的类型(Type)与种类(Kind)

在反射中关于类型划分为两种:类型(Type)和 种类(Kind)。因为在Go语言中我们可以使用 type 关键字构造很多自定义类型,而种类(Kind)就是指底层的类型,但在反射中,当需要区分指针、结构体等大品种的类型时,就会用到种类(Kind)。

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

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

// src/reflect/type.go
// A Kind represents the specific kind of type that a Type represents.
// The zero Kind is not a valid kind.
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            //底层指针
)

// String returns the name of k.
func (k Kind) String() string {
    if int(k) < len(kindNames) {
        return kindNames[k]
    }
    return "kind" + strconv.Itoa(int(k))
}

var kindNames = []string{
    Invalid:       "invalid",
    Bool:          "bool",
    Int:           "int",
    Int8:          "int8",
    Int16:         "int16",
    Int32:         "int32",
    Int64:         "int64",
    Uint:          "uint",
    Uint8:         "uint8",
    Uint16:        "uint16",
    Uint32:        "uint32",
    Uint64:        "uint64",
    Uintptr:       "uintptr",
    Float32:       "float32",
    Float64:       "float64",
    Complex64:     "complex64",
    Complex128:    "complex128",
    Array:         "array",
    Chan:          "chan",
    Func:          "func",
    Interface:     "interface",
    Map:           "map",
    Ptr:           "ptr",
    Slice:         "slice",
    String:        "string",
    Struct:        "struct",
    UnsafePointer: "unsafe.Pointer",
}

注意》Map、Slice、Chan 都属于引用类型,使用起来类似于指针,但是在种类常量定义中仍然属于独立的种类,不属于 Ptr。例如,上面定义的 type A struct{} 结构体,属于 Struct 种类,但是 *A 属于 Ptr 种类。

获取反射类型对象的类型名称使用 reflect.Type 中的 Name()方法,返回表示类型名称的字符串。

获取反射类型对象的种类名称使用 reflect.Type 中的 Kind()方法,返回reflect.Kind类型的常量。而 Kind类型实现了fmt.Stringer接口,即实现了该接口唯一的方法String(),当使用fmt.Printf / fmt.Println 格式化输出时,就能输出对应种类(Kind)名称的字符串。

示例:从反射类型对象中获取类型名称和种类名称的例子。

package main

import (
    "fmt"
    "reflect"
)

// 自定义类型 myInt
type myInt int64

func reflectType(x interface{}) {
    t := reflect.TypeOf(x)  // 获取x的反射类型对象
    fmt.Printf("type:%v, kind:%v\n", t.Name(), t.Kind())  //打印反射类型对象的类型和种类名称
}

func main() {
    var a *float32 // 指针
    var b myInt    // 自定义类型
    var c rune     // 类型别名
    reflectType(a) // type:, kind:ptr
    reflectType(b) // type:myInt, kind:int64
    reflectType(c) // type:int32, kind:int32

    type person struct {
        name string
        age  int
    }
    var d = person{
        name: "沙河小王子",
        age:  18,
    }
    
    type book struct{ title string }
    var e = book{title: "golang"}
    
    reflectType(d) // type:person, kind:struct
    reflectType(e) // type:book, kind:struct
}

运行结果:

type:, kind:ptr
type:myInt, kind:int64
type:int32, kind:int32
type:person, kind:struct
type:book, kind:struct

《说明》Go语言的反射类型对象中,像数组、通道、切片、map、指针等类型的变量,它们的 .Name() 都是返回空。

2.2 通过指针变量获取反射对象

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

package main

import (
    "fmt"
    "reflect"
)

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

运行结果:

1. typeOfCat type: '*main.Cat'
name:'', kind:'ptr'
2. typeOfCat type: 'main.Cat'
element name:'Cat', element kind:'struct'

《代码说明》

1、当 reflect.TypeOf() 传入的是一个指针类型参数时,返回的反射类型对象的动态类型也是指针类型的,即上例中的 *main.Cat。

2、typeOfCat.Elem() 中的 typeOfCat 具体类型必须是 Array, Chan, Map, Ptr, or Slice 中的一种,否则会造成panic,导致程序崩溃。typeOfCat.Elem() 实际上就是取指针类型的元素类型,如本例中,*main.Cat 指针类型的元素类型就是 main.Cat 类型。

3 反射的值对象 — reflect.Value

反射不仅可以获取值的类型信息,还可以动态地获取或者设置变量的值。Go语言中使用 reflect.Value 获取和设置变量的值。reflect.Value 是定义在reflect 包中的一个结构体类型,而reflect.Type 是一个接口类型,这点需要注意一下。该结构体类型提供了一些列方法(method)给使用者。

// src/reflect/value.go
type Value struct {
    // typ holds the type of the value represented by a Value.
    typ *rtype

    // Pointer-valued data or, if flagIndir is set, pointer to data.
    // Valid when either flagIndir is set or typ.pointers() is true.
    ptr unsafe.Pointer

    // flag holds metadata about the value.
    // The lowest bits are flag bits:
    //  - flagStickyRO: obtained via unexported not embedded field, so read-only
    //  - flagEmbedRO: obtained via unexported embedded field, so read-only
    //  - flagIndir: val holds a pointer to the data
    //  - flagAddr: v.CanAddr is true (implies flagIndir)
    //  - flagMethod: v is a method value.
    // The next five bits give the Kind of the value.
    // This repeats typ.Kind() except for method values.
    // The remaining 23+ bits give a method number for method values.
    // If flag.kind() != Func, code can assume that flagMethod is unset.
    // If ifaceIndir(typ), code can assume that flagIndir is set.
    flag

    // A method value represents a curried method invocation
    // like r.Read for some receiver r. The typ+val+flag bits describe
    // the receiver r, but the flag's Kind bits say Func (methods are
    // functions), and the top bits of the flag give the method number
    // in r's type's method table.
}

reflect.Value 总共有3个字段,一个是值的类型指针 typ,另一个是指向值的指针ptr,最后一个是标记字段 flag。

反射包中使用 reflect.ValueOf() 函数获取实例的值信息。reflect.ValueOf() 函数原型如下:

// src/reflect/value.go
func ValueOf(i interface{}) Value

reflect.ValueOf() 函数可以接受任意的值,并将接口的动态值以 reflect.Value 的形式返回。与reflect.TypeOf类似,reflect.ValueOf 的返回值也都是具体值。

另一个与 reflect.Type 类似的是,reflect.Value 也实现了 fmt.Stringer 接口,但除非传给ValueOf() 的是一个字符串,否则String() 方法的结果仅仅暴露的是类型。通常,需要使用fmt 包的 %v 格式符,它对reflect.Value 会进行特殊处理。

示例代码:

package main

import (
    "fmt"
    "reflect"
)

func main(){
    v1 := reflect.ValueOf(3)  //参入一个整数值
    fmt.Println(v1)
    fmt.Printf("%v\n", v1)
    fmt.Println(v1.String())
    
    v2 := reflect.ValueOf("golang")  //传入一个字符串
    fmt.Println(v2)
    fmt.Printf("%v\n", v2)
    fmt.Println(v2.String())
}

运行结果:

3
3
<int Value>
golang
golang
golang

调用 reflect.Value 类型的 Type() 方法,会把它的类型以 reflect.Type 方式返回。示例代码如下:

package main

import (
    "fmt"
    "reflect"
)

func main(){
    v := reflect.ValueOf(3)
    fmt.Printf("v type: %T\n", v)
    t := v.Type()
    fmt.Printf("t type: %T\n", t)
    fmt.Println(t.String())
}

运行结果:

v type: reflect.Value
t type: *reflect.rtype
int

reflect.ValueOf 的逆操作是 reflect.Value.Interface() 方法。它返回一个 interface{} 接口值,与reflect.Value 包含同一个具体值。但是,需要注意的是,不能直接从空接口变量中取值,需要先进行类型断言后才能取值。示例代码如下:

package main

import (
    "fmt"
    "reflect"
)

func main(){
    v := reflect.ValueOf(3)
    fmt.Printf("v type: %T\n", v)
    x := v.Interface()
    //fmt.Printf("x = %d\n", x)  //不需要进行类型断言也可以输出: x = 3
    i := x.(int)  //对空接口变量x进行类型断言
    fmt.Printf("i = %d\n", i)
}

运行结果:

v type: reflect.Value
i = 3

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

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

反射值获取原始值的方法
方法名说明
Interface interface{}将值以interface{}类型返回,可以通过类型断言转换为指定类型
Int() int64将值以int64类型返回,所有有符号整型均可以使用此方式返回
Uint() uint64将值以uint64类型返回,所有无符号整型均可以使用此方式返回
Float() float64将值以双精度(float64)类型返回,所有浮点数(float32、float64)均可以使用此方式返回
Bool() bool将值以bool类型返回
Byte() []bytes将值以字节数组[]bytes类型返回
String() string将值以字符串类型返回

示例:从反射值对(reflect.Value)中获取值的例子。

package main

import (
    "fmt"
    "reflect"
)

func main(){
    //声明整型变量a并初始化
    var a int = 1024
    //获取变量a的反射值对象
    valueOfA := reflect.ValueOf(a)
    
    //方式1-获取interface{}类型的值,然后通过类型断言转换
    var getA1 int = valueOfA.Interface().(int)
    fmt.Printf("getA1 = %d\n", getA1)
    
    //方式2-使用Int()方法获取64位的值,然后强制类型转换为int类型
    var getA2 int = int(valueOfA.Int())
    fmt.Printf("getA2 = %d\n", getA2)
}

运行结果:

getA1 = 1024
getA2 = 1024

3.2 反射对象的空和有效性判断

反射值对象提供了一系列方法进行零值和空判断。如下表所示:

反射值对象的零值和有效性判断方法
方法说明
IsNil() bool返回值是否为nil。如果值类型不是通道、函数、接口、map、指针或者切片时发生panic。类似于语言层的“v == nil”操作
IsValid() bool返回值是否有效。当值本身非法时,返回false。例如,reflect.Value 不包含任何值,值为nil

示例:反射值对象的零值和有效性判断例子。

package main

import (
    "fmt"
    "reflect"
)

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

运行结果:

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

3.3 使用反射值对象修改变量的值

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

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

使用reflect.Value 取元素、取地址以及修改值的属性方法如下表所示:

反射值对象的判定及获取元素的方法
方法名说明
Elem() Value取指针值指向的元素值,类似于语言层“*”操作。当值类型不是指针或是接口时发生宕机,空指针时返回nil的Value
Addr() Value对可寻址的值返回其地址,类似于语言层“&”操作。当值不可寻址时发生宕机
CanAddr() bool判断值是否可以被寻址
CanSet() bool返回值能否被修改。要求值可寻址且是导出的字段

2. 值修改相关方法

想要在函数中通过反射值对象来修改变量的值,需要注意函数参数传递的是值拷贝,必须传递变量地址才能修改变量的值。而反射中使用专有的 Elem() 方法来获取指针对象的值。

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

反射值对象修改值的方法集
Set(x Value)将值设置为传入的反射值对象的值
SetInt(x int64)使用int64设置值。当值的类型不是int、int8、int16、int32、int64时会发生宕机
SetUint64(x uint64)使用uint64设置值。当值的类型不是uint、uint8、uint16、uint32、uint64时会发生宕机
SetFloat(x float64)使用float64设置值。当值的类型不是float32、float64时会发生宕机
SetBool(x bool)使用bool设置值。当值的类型不是bool时会发生宕机
SetBytes(x []bytes)设置字节数组[]bytes值。当值的类型不是[]bytes时会发生宕机
SetString(x string)设置字符串值。当值的类型不是string时会发生宕机

以上方法,在 reflect.Value 的 CanSet()方法返回 false 时,仍然修改值时会发生宕机。在已知值的类型时,应尽量使用值对应类型的反射设置值方法。

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

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

package main

import (
    "fmt"
    "reflect"
)

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

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

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

报错意思是:SetInt 正在使用一个不能被寻址的值。因为从reflect.ValueOf()方法传入的是a的值,而不是变量a的地址,这个reflect.Value 反射值对象当然是不能被寻址的了。将代码修改一下:

func main(){
    //声明一个整型变量a并赋初值
    var a int = 1024
    
    //获取变量a的反射值对象(传入a的地址)
    valueOfA := reflect.ValueOf(&a)
    fmt.Printf("1--valueOfA type: %T\n", valueOfA)
    
    //反射中使用Elem()方法取出变量a地址对应的元素值(即a的值)
    valueOfA = valueOfA.Elem()
    fmt.Printf("2--valueOfA type: %T\n", valueOfA)

    //尝试将a修改为1
    valueOfA.SetInt(1)
    
    //打印a的值
    fmt.Println("a = ", valueOfA.Int()) //方法1
    fmt.Println("a = ", a)              //方法2
}

运行结果:

1--valueOfA type: reflect.Value
2--valueOfA type: reflect.Value
a =  1
a =  1

《代码说明》

  • valueOfA := reflect.ValueOf(&a): 将变量a取地址后传给 reflect.ValueOf() 方法。此时,reflect.ValueOf() 返回的 valueOfA 反射值对象持有变量a的地址。valueOfA 的类型是 reflect.Value。
  • valueOfA = valueOfA.Elem():使用reflect.Value类型的Elem()方法获取a地址的元素,也就是a的值。reflect.Value 的Elem() 方法返回的值类型也是 reflect.Value。此时valueOfA表示的是a的值且可以寻址。使用SetInt()方法设置值时不会发生崩溃。

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

3.4 通过类型创建类型的实例

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

 示例:通过类型创建类型的实例的例子。

package main

import (
    "fmt"
    "reflect"
)

func main()  {
    var a int
    
    //获取变量a的反射类型对象
    typeOfA := reflect.TypeOf(a)
    fmt.Printf("typeOfA type: %T\n", typeOfA)
    
    //根据反射类型对象创建类型实例
    aIns := reflect.New(typeOfA)
    fmt.Printf("aIns type: %T\n", aIns)
    
    //输出aIns的类型和种类
    fmt.Printf("aIns.Type=%v, aIns.Kind=%v\n", aIns.Type(), aIns.Kind())
}

运行结果:

typeOfA type: *reflect.rtype
aIns type: reflect.Value
aIns.Type=*int, aIns.Kind=ptr

《代码说明》

  • aIns := reflect.New(typeOfA) 使用reflect.New() 函数传入变量a的反射类型对象,创建了这个类型的实例值,值以reflect.Value 类型返回。这步操作等效于:new(int),因此返回的是 *int 类型的实例。可以看到,aIns 的类型为 *int,种类为指针。
  • reflect.New() 函数的定义如下:
//源码位置: src/reflect/value.go
// New returns a Value representing a pointer to a new zero value
// for the specified type. That is, the returned Value's Type is PtrTo(typ).
func New(typ Type) Value {
    if typ == nil {
        panic("reflect: New(nil)")
    }
    t := typ.(*rtype)
    ptr := unsafe_New(t)
    fl := flag(Ptr)
    return Value{t.ptrTo(), ptr, fl}
}

3.5 使用反射调用函数

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

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

package main

import (
    "fmt"
    "reflect"
)

//定义add函数
func add(a int, 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.Printf("retList type: %T\n", retList) // retList type: []reflect.Value
    
    //获取第一个返回值,取整数值
    fmt.Println(retList[0].Int())  // 30
}

运行结果:

retList type: []reflect.Value
30

《代码说明》

  • funcValue := reflect.ValueOf(add) 将add函数包装为反射值对象
  • paramList := []reflect.Value{reflect.ValueOf(10), reflect.ValueOf(20)} 将10 和 20 这两个整型值使用 reflect.ValueOf() 函数包装为 reflect.Value,再将反射值对象的切片 []reflect.Value 作为函数的参数。
  • retList := funcValue.Call(paramList) 使用函数的反射值对象的Call()方法,传入参数列表 paramList 达到调用add()函数的目的。
  • retList[0].Int()  调用成功后,通过 retList[0]取返回值的第一个参数,使用Int() 方法取返回值的整数值。

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

4 结构体反射

 4.1 使用反射获取结构体成员的成员类型

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

结构体成员访问的方法列表
方法说明
Field(i int) StructField根据索引,返回索引对应的结构体字段的信息。当值不是结构体或索引越界时发生宕机
NumField() int返回结构体成员字段数量。当类型不是结构体或索引越界时发生宕机
FiledByName(name string)  (StructField, bool)根据给定字符串返回字符串对应的结构体字段的信息。没有找到时,bool返回false,当类型不是结构体或索引越界时发生宕机
FieldByIndex(index []int)  StructField多层成员访问时,根据 [ ]int 提供的每个结构体的字段索引,返回结构体字段信息。没有找到时返回零值。当类型不是结构体或索引越界时发生宕机
FieldByNameFunc(match func(string) bool)  (Structfield, bool)根据匹配函数匹配需要的字段。当值不是结构体或是索引越界时发生宕机

1. 结构体字段类型(StructField)

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

// 源码位置: src/reflect/type.go
// A StructField describes a single field in a struct.
type StructField struct {
    // Name is the field name.
    Name string         //字段名
    // PkgPath is the package path that qualifies a lower case (unexported)
    // field name. It is empty for upper case (exported) field names.
    // See https://golang.org/ref/spec#Uniqueness_of_identifiers
    PkgPath string      //字段路径

    Type      Type      // field type(字段反射类型对象)
    Tag       StructTag // field tag string(字段的结构体标签)
    Offset    uintptr   // offset within struct, in bytes(字段在结构体中的相对偏移量)
    Index     []int     // index sequence for Type.FieldByIndex(reflect.Type的FiledByIndex
                        // 方法中返回的索引值切片)
    Anonymous bool      // is an embedded field(是否为匿名字段)
}

字段说明如下:

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

2. 获取结构体成员的类型信息

当我们使用 reflect.TypeOf() 函数得到一个结构体数据之后可以通过索引依次获取其字段信息,也可以通过字段名去获取指定的字段信息。

示例:反射访问结构体成员类型及其信息。

package main

import (
    "fmt"
    "reflect"
)

func main()  {
    //声明一个Cat结构体
    type Cat struct {
        Name string
        Type int `json:"type" id:"100"`
    }
    //创建一个Cat实例
    ins := Cat{
        Name: "mimi",
        Type: 1,
    }
    fmt.Printf("ins type: %T\n", ins)
    
    //获取结构体实例的反射类型对象
    typeOfCat := reflect.TypeOf(ins)
    fmt.Printf("typeOfCat type: %T\n", typeOfCat)
    //遍历结构体成员
    for i:=0; i < typeOfCat.NumField(); i++ {
        //获取每个结构体成员的字段类型
        fieldType := typeOfCat.Field(i)  //fieldType变量的类型为StructField
        //输出成员名和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"))
    }
}

运行结果:

ins type: main.Cat
typeOfCat type: *reflect.rtype
name: Name, tag: ''
name: Type, tag: 'json:"type" id:"100"'
type 100

《代码说明》

  • reflect.Type 中的Field() 方法和 NumField() 方法一般都是配对使用,用来实现对结构体成员的遍历操作。
  • 使用 StructField 结构体中的 Get() 方法,根据Tag 中的名字进行标签信息的提取。

4.2 结构体标签(struct Tag) — 对结构体字段的额外信息标签

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

JSON、BSON等格式进行序列化及对象关系映射(Object Relational Mapping,简称ORM)系统都会用到结构体标签,这些系统使用标签设定字段在处理时具备的特殊属性和可能发生的行为。这些信息都是静态的,无须实例化结构体,可以通过反射获取到。

1. 结构体标签的书写格式

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

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

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

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

func main()  {
    //声明一个Cat结构体
    type Cat struct {
        Name string
        Type int `json: "type" id:"100"`
    }
    typeOfCat := reflect.TypeOf(Cat{})
    //通过字段名找到字段类型信息
    if catType, ok := typeOfCat.FieldByName("Type"); ok {
        //从tag中取出需要的tag
        fmt.Println(catType.Tag.Get("json"), catType.Tag.Get("id"))
    }
}

代码输出空字符串,并不会输出期望的值。这是因为在 Type int `json: "type" id:"100"` 中,在 json: 和 "type" 之间增加了一个空格。这种写法没有遵守结构体标签的书写规则,因此无法通过 Tag.Get()方法获取到正确的json对应的值。这个错误在开发中非常容易被疏忽,造成难以察觉的错误。

修改后的代码:

func main()  {
    //声明一个Cat结构体
    type Cat struct {
        Name string
        Type int `json:"type" id:"100"`
    }
    typeOfCat := reflect.TypeOf(Cat{})
    //通过字段名找到字段类型信息
    if catType, ok := typeOfCat.FieldByName("Type"); ok {
        //从tag中取出需要的tag
        fmt.Println(catType.Tag.Get("json"), catType.Tag.Get("id"))
    }
}

运行结果:

type 100

2. 从结构体标签获取值

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

//源码位置: src/reflect/type.go
type StructTag string  //StructTag类型实际上是string类型

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中的键,查询值是否存在

示例:

package main

import (
    "fmt"
    "reflect"
)

type student struct {
    Name  string `json:"name"`
    Score int    `json:"score"`
}

func main() {
    stu := student{
        Name:  "小王子",
        Score: 90,
    }

    t := reflect.TypeOf(stu)
    //打印t的类型和种类
    fmt.Println(t.Name(), t.Kind()) // student struct
    // 通过for循环遍历结构体的所有字段信息
    for i := 0; i < t.NumField(); i++ {
        field := t.Field(i)
        fmt.Printf("name:%s index:%d type:%v json tag:%v\n", field.Name, field.Index, field.Type, field.Tag.Get("json"))
    }

    // 通过字段名获取指定结构体字段信息
    if scoreField, ok := t.FieldByName("Score"); ok {
        fmt.Printf("name:%s index:%d type:%v json tag:%v\n", scoreField.Name, scoreField.Index, scoreField.Type, scoreField.Tag.Get("json"))
    }
}

运行结果:

student struct
name:Name index:[0] type:string json tag:name
name:Score index:[1] type:int json tag:score
name:Score index:[1] type:int json tag:score

4.3 使用反射访问结构体的成员字段的值

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

反射值对象的成员访问方法
方法说明
Field(i int) Value根据索引,返回返回索引对应的结构体成员字段的反射值对象。当值不是结构体或索引越界时发生宕机
NumField() int返回结构体成员字段数量。当值不是结构体或索引越界时发生宕机
FieldByName(name string) Value根据给定字符串返回字符串对应结构体字段的反射值对象。没有找到时返回零值。
FieldByIndex(index []int) Value多层成员访问时,根据[ ]int 提供的每个结构体的字段索引,返回字段的反射值对象。没有找到时返回零值。
FieldByNameFunc(match func(string) bool) Value根据匹配函数匹配需要的字段,返回字段的反射值对象。没有找到时返回零值。

示例:反射获取结构体成员字段的值。

package main

import (
    "fmt"
    "reflect"
)

//定义结构体
type Dummy struct{
    a int
    b string
    float32
    bool
    next *Dummy  //嵌入字段
}

func main() {
    //值包装结构体,变量d的类型为reflect.Value
    d := reflect.ValueOf(Dummy{
        next: &Dummy{},
    })
    fmt.Printf("d type: %T\n", d)
    //获取字段数量
    fmt.Println("NumField =", d.NumField())

    //获取索引为2的字段(float32)
    floatField := d.Field(2)
    //输出字段类型
    fmt.Println("floatField.Type():", 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())
}

运行结果:

d type: reflect.Value
NumField = 5
floatField.Type(): float32
FieldByName("b").Type(): string
FieldByIndex([]int{4, 0}).Type(): int

《代码说明》[]int{4, 0} 表示在Dummy结构体中索引值为4的成员,也就是 next,而next的类型为*Dummy,也是一个结构体,因此使用 []int{4, 0} 中的0继续在next值的基础上索引,结构在Dummy中索引值为0的是a字段,其类型为int。

4.4 使用反射值对象修改结构体成员字段的值

结构体成员字段的值,如果字段没有被导出,即便不使用反射也可以被访问,但是不能通过反射修改。

示例代码如下:

package main

import (
    "fmt"
    "reflect"
)

func main()  {
    type Dog struct {
        legCount int    //注意字段的首字母是小写
    }
    //获取Dog实例地址的反射值对象
    valueOfDog := reflect.ValueOf(Dog{})
    fmt.Printf("valueOfDog type: %T\n", valueOfDog)
    
    //获取legCount字段的反射值对象
    vLegCount := valueOfDog.FieldByName("legCount")
    
    //尝试设置legCount字段值(这里会发生崩溃)
    vLegCount.SetInt(4)
    
    fmt.Println("legCount =", vLegCount.Int())
}

运行结果:

valueOfDog type: reflect.Value
panic: reflect: reflect.Value.SetInt using value obtained using unexported field   //发生了崩溃

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

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

func main()  {
    type Dog struct {
        LegCount int    //注意字段的首字母是大写
    }
    //获取Dog实例地址的反射值对象
    valueOfDog := reflect.ValueOf(Dog{})
    fmt.Printf("valueOfDog type: %T\n", valueOfDog)
    
    //获取legCount字段的反射值对象
    vLegCount := valueOfDog.FieldByName("LegCount")
    
    //尝试设置legCount字段值(这里会发生崩溃)
    vLegCount.SetInt(4)
    
    fmt.Println("LegCount =", vLegCount.Int())
}

运行结果:

valueOfDog type: reflect.Value
panic: reflect: reflect.Value.SetInt using unaddressable value    //程序再次发生崩溃

这个错误的意思是说:SetInt() 使用了一个不能被寻址的地址,因此其字段也无法被修改。也就是上文中提到的通过反射修改变量值的前提条件:这个值必须可以被寻址。因此,我们需要将Dog结构体实例的地址传入 reflect.ValueOf() 函数中,然后通过 reflect.Value 的 Elem() 方法获取到Dog结构体对应的反射值对象。修改后的代码如下:

func main()  {
    type Dog struct {
        LegCount int    //注意字段的首字母是大写
    }
    //获取Dog实例地址的反射值对象
    valueOfDog := reflect.ValueOf(&Dog{})  //传入Dog实例的地址
    fmt.Printf("valueOfDog type: %T\n", valueOfDog)
    
    //取出Dog实例指针指向的结构体值对象
    valueOfDog = valueOfDog.Elem()
    
    //获取legCount字段的反射值对象
    vLegCount := valueOfDog.FieldByName("LegCount")
    
    //尝试设置legCount字段值(这里会发生崩溃)
    vLegCount.SetInt(4)
    
    fmt.Println("LegCount =", vLegCount.Int())  //方法1
    fmt.Println("LegCount =", vLegCount)        //方法2
}

运行结果:

valueOfDog type: reflect.Value
LegCount = 4
LegCount = 4

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

(1)取这个变量的地址或者这个变量所在的结构体已经是指针类型。

(2)使用 reflect.ValueOf() 函数进行值包装得到反射值对象。

(3)通过 Value.Elem() 方法获得指针值指向的元素值对象(reflect.Value),因为反射值对象(reflect.Value)内部对象为指针时,使用Set系列方法设置时会报出宕机错误。

(4)使用 Value.Set 系列方法设置值。

5 反射的优缺点

5.1 反射的优点

1. 通用性

特别是一些类库和框架代码需要一种通用的处理模型,而不是针对每一种场景硬编码处理,此时借助反射可以极大地简化设计。

2. 灵活性

反射提供了一种程序了解自己和改变自己的能力,这为一些测试工具的开发提供了有力的支持。

5.2 反射的缺点

1. 反射是脆弱的

由于反射可以在程序运行时修改程序的状态,这种修改没有经过编译器的严格检查,不正确的修改很容易导致程序的崩溃。

2. 反射是晦涩难懂的

语言的反射接口由于设计语言的运行时,没有具体的类型系统的约束,接口的抽象级别高,但实现细节复杂,导致使用反射的代码难以阅读和理解。

3. 反射有部分性能损失

反射提供动态修改程序状态的能力,必须不是直接的地址引用,而是要借助运行时构造一个抽象层,这种间接访问会有性能的损失。基于反射实现的代码通常比正常代码运行速度慢一到两个数量级。

6 反射的最佳实践

(1)在库或框架内部使用反射,而不是把反射接口暴露给调用者,复杂性留在内部,简单性放在接口。

(2)框架代码才考虑使用反射,一般的业务代码没必要抽象到反射的层次,这种过度设计会带来复杂度的提升,使得代码难以维护。

(3)除非没有其他办法,否则尽量不要使用反射技术。

参考

Go语言基础之反射

Go语言reflect包源码

《Go语言从入门到进阶实战(视频教学版)》

《Go语言核心编程》

《Go程序设计语言》

《Go语言学习笔记》

  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在 Go 语言中,反射是一种机制,可以在程序运行时动态获取变量的类型信息和值信息,并可以对其进行修改。反射可以使代码更加灵活,但也会带来一定的性能损失。 Go 语言中的反射主要通过 `reflect` 包来实现。`reflect` 包提供了两个重要的类型:`Type` 和 `Value`,分别表示类型信息和值信息。通过 `Type` 和 `Value`,我们可以获取变量的类型信息和值信息,并可以对其进行修改。 下面是一个使用反射获取变量类型和值的示例代码: ```go package main import ( "fmt" "reflect" ) func main() { var x float64 = 3.14 fmt.Println("type:", reflect.TypeOf(x)) fmt.Println("value:", reflect.ValueOf(x)) } ``` 在上面的示例代码中,我们定义了一个 `float64` 类型的变量 `x`,然后使用 `reflect.TypeOf()` 和 `reflect.ValueOf()` 函数分别获取变量的类型信息和值信息,并打印出来。由于 `reflect.TypeOf(x)` 返回的是一个 `reflect.Type` 类型的值,因此我们需要使用 `fmt.Println()` 函数来打印其名称。而 `reflect.ValueOf(x)` 返回的是一个 `reflect.Value` 类型的值,因此我们可以直接使用 `%v` 格式化符号来打印其值。 除了获取变量的类型信息和值信息之外,反射还可以用于动态调用函数和修改变量的值。例如,通过 `Value` 类型的 `Elem()` 方法可以获取指向变量的指针,并使用 `Set()` 方法来修改变量的值。 下面是一个使用反射动态修改变量值的示例代码: ```go package main import ( "fmt" "reflect" ) func main() { var x float64 = 3.14 v := reflect.ValueOf(&x).Elem() v.SetFloat(6.28) fmt.Println(x) } ``` 在上面的示例代码中,我们首先定义了一个 `float64` 类型的变量 `x`,然后使用 `reflect.ValueOf(&x).Elem()` 获取指向变量的指针,并使用 `SetFloat()` 方法将其值修改为 `6.28`。最后,我们打印出变量 `x` 的值,可以看到其值已经被修改为 `6.28`。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值