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       uintptrptrdata    uintptr // number of bytes in the type that can contain pointershash       uint32  // hash of type; avoids computation in hash tablestflag      tflag   // extra type information flagsalign      uint8   // alignment of variable with this typefieldAlign uint8   // alignment of struct field with this typekind       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) boolgcdata    *byte   // garbage collection datastr       nameOff // string formptrToThis typeOff // type for pointer to this type, may be zero
}// arrayType represents a fixed array type. -- 数组类型
type arrayType struct {rtypeelem  *rtype // array element typeslice *rtype // slice typelen   uintptr
}// chanType represents a channel type. -- 通道类型
type chanType struct {rtypeelem *rtype  // channel element typedir  uintptr // channel direction (ChanDir)
}// interfaceType represents an interface type. -- 接口类型
type interfaceType struct {rtypepkgPath name      // import pathmethods []imethod // sorted by hash
}// mapType represents a map type.  -- 映射类型
type mapType struct {rtypekey    *rtype // map key typeelem   *rtype // map element (value) typebucket *rtype // internal bucket structure// function for hashing keys (ptr to key, seed) -> hashhasher     func(unsafe.Pointer, uintptr) uintptrkeysize    uint8  // size of key slotvaluesize  uint8  // size of value slotbucketsize uint16 // size of bucketflags      uint32
}// ptrType represents a pointer type. -- 指针类型
type ptrType struct {rtypeelem *rtype // pointer element (pointed at) type
}// sliceType represents a slice type. -- 切片类型
type sliceType struct {rtypeelem *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 mainimport ("fmt""reflect"
)func main() {t := reflect.TypeOf(3)fmt.Println(t)             //输出:intfmt.Println(t.String())    //输出:int
}

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

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

package mainimport ("fmt""reflect""io""os"
)func main() {var w io.Writer = os.Stdout    //声明一个io.Writer的接口类型变量w,并初始化为 os.Stdoutfmt.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 uintconst (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 mainimport ("fmt""reflect"
)// 自定义类型 myInt
type myInt int64func 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:ptrreflectType(b) // type:myInt, kind:int64reflectType(c) // type:int32, kind:int32type person struct {name stringage  int}var d = person{name: "沙河小王子",age:  18,}type book struct{ title string }var e = book{title: "golang"}reflectType(d) // type:person, kind:structreflectType(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 mainimport ("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 mainimport ("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 mainimport ("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 mainimport ("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 = 3i := 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 mainimport ("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 mainimport ("fmt""reflect"
)func main(){// *int 的空指针var a *intfmt.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())//实例化一个mapm := 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 mainimport ("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修改为1valueOfA.SetInt(1)//打印a的值fmt.Println("a = ", valueOfA.Int()) //方法1fmt.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 mainimport ("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 mainimport ("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_identifiersPkgPath 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 mainimport ("fmt""reflect"
)func main()  {//声明一个Cat结构体type Cat struct {Name stringType 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//输出成员名和tagfmt.Printf("name: %v, tag: '%v'\n", fieldType.Name, fieldType.Tag)}//通过字段名找到字段类型信息if catType, ok := typeOfCat.FieldByName("Type"); ok {//从tag中取出需要的tagfmt.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 stringType int `json: "type" id:"100"`}typeOfCat := reflect.TypeOf(Cat{})//通过字段名找到字段类型信息if catType, ok := typeOfCat.FieldByName("Type"); ok {//从tag中取出需要的tagfmt.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 stringType int `json:"type" id:"100"`}typeOfCat := reflect.TypeOf(Cat{})//通过字段名找到字段类型信息if catType, ok := typeOfCat.FieldByName("Type"); ok {//从tag中取出需要的tagfmt.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 mainimport ("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 mainimport ("fmt""reflect"
)//定义结构体
type Dummy struct{a intb stringfloat32boolnext *Dummy  //嵌入字段
}func main() {//值包装结构体,变量d的类型为reflect.Valued := 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 mainimport ("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())  //方法1fmt.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语言学习笔记》

Go语言--反射(reflect)相关推荐

  1. go语言反射机制、reflect.TypeOf、 reflect.ValueOf、字符串处理(详解)

    文章目录 前言 一.反射基本概念 ①go语言反射为何而生? ②反射弊端 ③怎样使用反射机制? 一.反射使用到的库及常用函数 ①用到的库 ②常用的字符串处理函数 (1) 将字符串加载为固定的类型strc ...

  2. go struct 静态函数_Go语言学习笔记(四)结构体struct 接口Interface 反射reflect...

    加 Golang学习 QQ群共同学习进步成家立业工作 ^-^ 群号:96933959 结构体struct struct 用来自定义复杂数据结构,可以包含多个字段(属性),可以嵌套: go中的struc ...

  3. Golang的反射reflect深入理解和示例

    [TOC] Golang的反射reflect深入理解和示例 [记录于2018年2月] 编程语言中反射的概念 在计算机科学领域,反射是指一类应用,它们能够自描述和自控制.也就是说,这类应用通过采用某种机 ...

  4. Atitit.跨语言反射api 兼容性提升与增强 java c#。Net  php  js

    Atitit.跨语言反射api 兼容性提升与增强 java c#.Net  php  js 1. 什么是反射1 1.1.       反射提供的主要功能:2 1.2.       实现反射的过程:2 ...

  5. 反射 reflect

    反射 reflect 什么是反射,其实就是反省,自省的意思 反射指的是以一个对象应该具备,可以检测,修改,增加自身属性的能力 反射就是通过字符串操作属性 涉及到的四个函数,这四个函数就是普通的内置函数 ...

  6. 浅谈Java反射(Reflect)技术--常用方法

    Java反射(Reflect)技术 概念:动态获取在当前Java虚拟机中的类.接口或者对象等等信息(运行过程中读取内容) 1.作用(面试问题): 1.1 解除两个类之间的耦合性,即在未得到依赖类的情况 ...

  7. Go 语言编程 — reflect 反射机制

    目录 文章目录 目录 为什么需要反射? reflect 包 通过 reflect.TypeOf() 获取对象的反射类型 reflect.Type 通过 reflect.Elem() 获取指针所指向的对 ...

  8. Go语言类库-reflect(反射)

    概述 什么是反射? 反射是计算机程序在运行时可以访问,检查和修改本身状态或者行为的一种能力,大多数编程语言都支持反射.Go语言中,使用反射可以在程序执行过程中更新变量和检查对象的属性,调用对象的方法. ...

  9. 反射 Reflect Class 基础 API MD

    Markdown版本笔记 我的GitHub首页 我的博客 我的微信 我的邮箱 MyAndroidBlogs baiqiantao baiqiantao bqt20094 baiqiantao@sina ...

最新文章

  1. server sql 数据总行数_sql统计行数的语句
  2. 黑苹果没有找到触控板为什么还是能用_为什么Macbook触控板体验领先Windows那么多,却难以取代鼠标?...
  3. How to mount HFS EFI on macOS
  4. LeetCode - Easy - 118. Pascal‘s Triangle
  5. NOIP 2007 普及组初赛试题(C++)(无答案)
  6. python 操作微信闪电贷款_16、6个能够让Python程序快如闪电的小技巧
  7. PowerBuilder fileOpen()
  8. MYSQL 数据库维护常识
  9. 家用计算机常见故障及解决方式,计算机常见故障及解决方法
  10. ERP实施中需掌握的基本财务基础知识
  11. 阅兵方阵-蓝桥杯国赛
  12. 名人名言摘选-李嘉诚
  13. VS2016 调用matlab脚本 ——缺少mclmcr.dll
  14. Matlab迭代算法实现
  15. 【一】生成CA根证书、公钥、私钥指令(数字证书)
  16. 什么是长连接?长连接、短连接、三次握手
  17. windows安装IIS不成功的原因
  18. Developing Application Frameworks in .NET(隨書源碼下載地址)
  19. 微信小程序之界面交互API07
  20. python字典遍历的几种方式

热门文章

  1. 周志华教授《机器学习》中PCA求解错了?
  2. C语言入门教程||C语言常量||C语言存储类
  3. 金山毒霸6 、金山网镖6 增强版 发布在即!!!
  4. vue指令模式 添加埋点
  5. SCSS常用语法总结
  6. Python能做什么?
  7. 程序人生 - 桂林西瓜霜含片 西瓜霜清咽含片
  8. 【Matlab】简单的滑模控制程序及Simulink仿真
  9. AUTOCAD2008注册
  10. MySQL-修改数据库密码