反射的基本介绍:
- 反射可以在运行时动态获取变量的各种信息,比如变量的类型、类别。
- 如果是结构体变量,还可以获取到结构体本身的信息(包含结构体的字段、tag、方法等)。
- 通过反射可以修改变量的值,可以调用关联的方法。
- 反射在很多框架中都有使用,使用反射可以写出一些比较通用性的方法,比如json数据的编解码。
1、Golang反射中的一些定义
1.1 Kind
Kind代表Type类型值表示的具体分类。零值表示非法分类。
Type是类型,Kind是类别。Type和Kind可能相同,也可能不相同:
比如: var num int = 10 num的Type为int Kind也为Int
var stu Student stu的Type是包名.Student ,Kind是Struct
所以类别是更加广泛的,比如定义了很多不同类型结构体,它们的类别都是相同的,都是结构体 。
在reflet包中定义了如下类型的Kind,也就是变量的分类:
const (
Invalid Kind = iota
Bool
Int
Int8
Int16
Int32
Int64
Uint
Uint8
Uint16
Uint32
Uint64
Uintptr
Float32
Float64
Complex64
Complex128
Array
Chan
Func
Interface
Map
Ptr
Slice
String
Struct
UnsafePointer
)
1.2 Type
Type类型用来表示一个go类型。不是所有go类型的Type值都能使用所有方法。在调用有分类限定的方法时,应先使用Kind方法获知类型的分类。调用该分类不支持的方法会导致运行时的panic。
type中包含了一个变量的类型信息,比如对于一个结构体,type中包含了结构体的字段数量、每个字段、绑定的方法数量、每个方法以及每个方法的参数等。这些都是一个变量的类型所拥有的,而不是变量所拥有的,比如变量中存储的值,变量不同,值可能也就不同。但是对于每一个同类型的变量来说,它拥有的字段、方法都是相同的。
type Type interface {
// Kind返回该接口的具体分类
Kind() Kind
// Name返回该类型在自身包内的类型名,如果是未命名类型会返回""
Name() string
// PkgPath返回类型的包路径,即明确指定包的import路径,如"encoding/base64"
// 如果类型为内建类型(string, error)或未命名类型(*T, struct{}, []int),会返回""
PkgPath() string
// 返回类型的字符串表示。该字符串可能会使用短包名(如用base64代替"encoding/base64")
// 也不保证每个类型的字符串表示不同。如果要比较两个类型是否相等,请直接用Type类型比较。
String() string
// 返回要保存一个该类型的值需要多少字节;类似unsafe.Sizeof
Size() uintptr
// 返回当从内存中申请一个该类型值时,会对齐的字节数
Align() int
// 返回当该类型作为结构体的字段时,会对齐的字节数
FieldAlign() int
// 如果该类型实现了u代表的接口,会返回真
Implements(u Type) bool
// 如果该类型的值可以直接赋值给u代表的类型,返回真
AssignableTo(u Type) bool
// 如该类型的值可以转换为u代表的类型,返回真
ConvertibleTo(u Type) bool
// 返回该类型的字位数。如果该类型的Kind不是Int、Uint、Float或Complex,会panic
Bits() int
// 返回array类型的长度,如非数组类型将panic
Len() int
// 返回该类型的元素类型,如果该类型的Kind不是Array、Chan、Map、Ptr或Slice,会panic
Elem() Type
// 返回map类型的键的类型。如非映射类型将panic
Key() Type
// 返回一个channel类型的方向,如非通道类型将会panic
ChanDir() ChanDir
// 返回struct类型的字段数(匿名字段算作一个字段),如非结构体类型将panic
NumField() int
// 返回struct类型的第i个字段的类型,如非结构体或者i不在[0, NumField())内将会panic
Field(i int) StructField
// 返回索引序列指定的嵌套字段的类型,
// 等价于用索引中每个值链式调用本方法,如非结构体将会panic
FieldByIndex(index []int) StructField
// 返回该类型名为name的字段(会查找匿名字段及其子字段),
// 布尔值说明是否找到,如非结构体将panic
FieldByName(name string) (StructField, bool)
// 返回该类型第一个字段名满足函数match的字段,布尔值说明是否找到,如非结构体将会panic
FieldByNameFunc(match func(string) bool) (StructField, bool)
// 如果函数类型的最后一个输入参数是"..."形式的参数,IsVariadic返回真
// 如果这样,t.In(t.NumIn() - 1)返回参数的隐式的实际类型(声明类型的切片)
// 如非函数类型将panic
IsVariadic() bool
// 返回func类型的参数个数,如果不是函数,将会panic
NumIn() int
// 返回func类型的第i个参数的类型,如非函数或者i不在[0, NumIn())内将会panic
In(i int) Type
// 返回func类型的返回值个数,如果不是函数,将会panic
NumOut() int
// 返回func类型的第i个返回值的类型,如非函数或者i不在[0, NumOut())内将会panic
Out(i int) Type
// 返回该类型的方法集中方法的数目
// 匿名字段的方法会被计算;主体类型的方法会屏蔽匿名字段的同名方法;
// 匿名字段导致的歧义方法会滤除
NumMethod() int
// 返回该类型方法集中的第i个方法,i不在[0, NumMethod())范围内时,将导致panic
// 对非接口类型T或*T,返回值的Type字段和Func字段描述方法的未绑定函数状态
// 对接口类型,返回值的Type字段描述方法的签名,Func字段为nil
Method(int) Method
// 根据方法名返回该类型方法集中的方法,使用一个布尔值说明是否发现该方法
// 对非接口类型T或*T,返回值的Type字段和Func字段描述方法的未绑定函数状态
// 对接口类型,返回值的Type字段描述方法的签名,Func字段为nil
MethodByName(string) (Method, bool)
// 内含隐藏或非导出方法
}
reflect.TypeOf函数可以用来获取一个变量的Type。
1.3 Value
Value为go值提供了反射接口。不是所有go类型值的Value表示都能使用所有方法。在调用有分类限定的方法时,应先使用Kind方法获知该值的分类。
Value是一个结构体,它里面的字段我们不用关心。
Value绑定了一些方法,是用来获取一个变量的值属性的,比如变量的值,一个切片的长度、容量等。
type Value struct {
// typ 保存由 Value 表示的值的类型。
typ *rtype
// 指针值数据,或者,如果设置了 flagIndir,则指向数据的指针.
// 当设置 flagIndir 或 typ.pointers() 为真时有效。
ptr unsafe.Pointer
flag
}
reflect.ValueOf可以获取一个变量的value信息。
Value绑定的方法介绍:
// IsNil报告v持有的值是否为nil。v持有的值的分类必须是通道、函数、接口、映射、指针、切片之一;否则IsNil函数会导致panic。
func (v Value) IsNil() bool
// 获取v的Kind
func (v Value) Kind() Kind
// 也可以通过Value来获取Type
func (v Value) Type() Type
// 将v转换为类型t的值,并返回该值的Value封装。如果go转换规则不支持这种转换,会panic
func (v Value) Convert(t Type) Value
// Elem返回v持有的接口保管的值的Value封装,或者v持有的指针指向的值的Value封装。如果v的Kind不是Interface或Ptr会panic;
func (v Value) Elem() Value
获取当前Value持有的对象的值:
// 获取值的方法:
// 获取变量的值,如果调用的不是该变量类型所对应的方法就会panic,
// 比如一个变量为int类型但是调用了Float,那么就会panic
func (v Value) Bool() bool
func (v Value) Int() int64
func (v Value) Uint() uint64
func (v Value) Float() float64
func (v Value) Complex() complex128
func (v Value) Bytes() []byte
func (v Value) String() string
// 可以将任何类型转为interface
func (v Value) Interface() (i interface{})
/*
将v持有的值作为一个指针返回。本方法返回值不是unsafe.Pointer类型,
以避免程序员不显式导入unsafe包却得到
unsafe.Pointer类型表示的指针。如果v的Kind不是Chan、Func、Map、Ptr、Slice或UnsafePointer会panic。
如果v的Kind是Func,返回值是底层代码的指针,但并不足以用于区分不同的函数;
只能保证当且仅当v持有函数类型零值nil时,返回值为0。
如果v的Kind是Slice,返回值是指向切片第一个元素的指针。如果持有的切片为nil,返回值为0;
如果持有的切片没有元素但不是nil,返回值不会是0。
*/
func (v Value) Pointer() uintptr
// 返回v[i:j](v持有的切片的子切片的Value封装);如果v的Kind不是Array、Slice或String会panic。
// 如果v是一个不可寻址的数组,或者索引出界,也会panic
func (v Value) Slice(i, j int) Value
// 是Slice的3参数版本,返回v[i:j:k] ;如果v的Kind不是Array、Slice或String会panic。
// 如果v是一个不可寻址的数组,或者索引出界,也会panic。
func (v Value) Slice3(i, j, k int) Value
// 返回v持有值的容量,如果v的Kind不是Array、Chan、Slice会panic
func (v Value) Cap() int
// 返回v持有值的长度,如果v的Kind不是Array、Chan、Slice、Map、String会panic
func (v Value) Len() int
// 返回v持有值的第i个元素。如果v的Kind不是Array、Chan、Slice、String,或者i出界,会panic
func (v Value) Index(i int) Value
// 返回v持有值里key持有值为键对应的值的Value封装。如果v的Kind不是Map会panic。
// 如果未找到对应值或者v持有值是nil映射,会返回Value零值。key的持有值必须可以直接赋值给v持有值类型的键类型。
func (v Value) MapIndex(key Value) Value
// 返回一个包含v持有值中所有键的Value封装的切片,该切片未排序。如果v的Kind不是Map会panic。
// 如果v持有值是nil,返回空切片(非nil)。
func (v Value) MapKeys() []Value
给当前Value持有的对象赋值:
// 设置值的方法
// 同样,如果类型对不上就会panic
func (v Value) SetBool(x bool)
func (v Value) SetInt(x int64)
func (v Value) SetUint(x uint64)
func (v Value) SetFloat(x float64)
func (v Value) SetComplex(x complex128)
func (v Value) SetBytes(x []byte)
func (v Value) SetString(x string)
func (v Value) SetPointer(x unsafe.Pointer)
func (v Value) SetCap(n int)
func (v Value) SetLen(n int)
func (v Value) SetMapIndex(key, val Value)
// 将v的持有值修改为x的持有值。
func (v Value) Set(x Value)
结构体相关:
// 返回结构体的字段数,如果不是结构体 panic
func (v Value) NumField() int
// 返回结构体的第i个字段(的Value封装)。如果v的Kind不是Struct或i出界会panic
func (v Value) Field(i int) Value
// 返回索引序列指定的嵌套字段的Value表示,等价于用索引中的值链式调用本方法,如v的Kind非Struct将会panic
func (v Value) FieldByIndex(index []int) Value
// 返回该类型名为name的字段(的Value封装)(会查找匿名字段及其子字段),如果v的Kind不是Struct会panic;
// 如果未找到会返回Value零值。
func (v Value) FieldByName(name string) Value
// 返回该类型第一个字段名满足match的字段(的Value封装)(会查找匿名字段及其子字段),
// 如果v的Kind不是Struct会panic;如果未找到会返回Value零值
func (v Value) FieldByNameFunc(match func(string) bool) Value
// 返回v持有值的方法集的方法数目。
func (v Value) NumMethod() int
// 返回v持有值类型的第i个方法的已绑定(到v的持有值的)状态的函数形式的Value封装。
func (v Value) Method(i int) Value
// 回v的名为name的方法的已绑定(到v的持有值的)状态的函数形式的Value封装。
func (v Value) MethodByName(name string) Value
//Call方法使用输入的参数in调用v持有的函数。例如,如果len(in) == 3,
// v.Call(in)代表调用v(in[0], in[1], in[2])(其中Value值表示其持有值)。
// 如果v的Kind不是Func会panic。它返回函数所有输出结果的Value封装的切片
func (v Value) Call(in []Value) []Value
1.4 reflect中的其它定义
StructField是用来保存一个结构体中的一个字段的相关信息的,例如字段名、字段类型、字段标签等。通过Type的Field()方法可以获取到一个StrucetField结构体。
type StructField struct {
// Name是字段的名字。PkgPath是非导出字段的包路径,对导出字段该字段为""。
Name string
PkgPath string
Type Type // 字段的类型
Tag StructTag // 字段的标签
Offset uintptr // 字段在结构体中的字节偏移量
Index []int // 用于Type.FieldByIndex时的索引切片
Anonymous bool // 是否匿名字段
}
type StructTag string
// Get方法返回标签字符串中键key对应的值。如果标签中没有该键,会返回""。
func (tag StructTag) Get(key string) string
Method是用来保存一个方法的相关信息的,通过Type的Method()方法可以获取该结构体。
type Method struct {
// Name是方法名。PkgPath是非导出字段的包路径,对导出字段该字段为""。
// 结合PkgPath和Name可以从方法集中指定一个方法。
Name string
PkgPath string
Type Type // 方法类型
Func Value // 方法的值
Index int // 用于Type.Method的索引
}
创建变量用的函数:
// New返回一个Value类型值,该值持有一个指向类型为typ的新申请的零值的指针,返回值的Type为PtrTo(typ)。
func New(typ Type) Value
// MakeSlice创建一个新申请的元素类型为typ,长度len容量cap的切片类型的Value值。
func MakeSlice(typ Type, len, cap int) Value
// MakeMap创建一个特定映射类型的Value值。
func MakeMap(typ Type) Value
// MakeChan创建一个元素类型为typ、有buffer个缓存的通道类型的Value值。
func MakeChan(typ Type, buffer int) Value
func MakeFunc(typ Type, fn func(args []Value) (results []Value)) Value
赋值函数:
// 向切片类型的Value值s中添加一系列值,x等Value值持有的值必须能直接赋值给s持有的切片的元素类型。
func Append(s Value, x ...Value) Value
// 类似Append函数,但接受一个切片类型的Value值。将切片t的每一个值添加到s。
func AppendSlice(s, t Value) Value
1.5 Value和Type的关系
Value与Type是相互关联的,通过reflect.TypeOf可以获取变量的类型信息,通过Value.Type也可以获取变量的类型信息。
在反射中变量、interface{}和reflect.Value是可以相互转换的:
变量、interface{}和reflect.Value的相互转换:
1.6 判断变量的类别
不管是通过Value还是Type我们都可以调用Kind()函数来获取一个变量的类别,我们可以使用switch来判断变量的类别,对不同的类型执行不同的操作:
switch reflect.ValueOf(meta).Kind() {
case reflect.String:
...
case reflect.Int:
...
case reflect.Struct:
...
case reflect.Map:
...
}
1.7 反射中的注意事项
通过反射来修改变量,注意当使用SetXX方法来设置值需要通过对应的指针类型来完成,这样才能改变传入的变量的值,而且在传入参数时,也要传入指针,需要用到reflect.ValueOf().Elem()来获取。
// Elem返回v持有的接口保管的值的Value封装,或者v持有的指针指向的值的Value封装。如果v的Kind不是Interface或Ptr会panic;
func (v Value) Elem() Value
比如下面的代码:
package main
import (
"fmt"
"reflect"
)
// 第一种
func main() {
var a int = 0
SetValue(a) // 不使用指针
fmt.Println(a)
}
func SetValue(v interface{}) {
elem := reflect.ValueOf(v) // 不使用Elem()
elem.SetInt(666);
}
// 报错
panic: reflect: reflect.Value.SetInt using unaddressable value
// 第二种
func main() {
var a int = 0
SetValue(&a) // 使用指针
fmt.Println(a)
}
func SetValue(v interface{}) {
elem := reflect.ValueOf(v) // 但是没使用Elem
elem.SetInt(666);
}
// 报错
panic: reflect: reflect.Value.SetInt using unaddressable value
// 第三种
func main() {
var a int = 0
SetValue(a) // 没使用指针
fmt.Println(a)
}
func SetValue(v interface{}) {
elem := reflect.ValueOf(v).Elem() // 使用了Elem()
elem.SetInt(666);
}
// 报错
panic: reflect: call of reflect.Value.Elem on int Value
// 因此,如果要修改变量,需要使用指针,同时使用Elem()返回的Value来修改
func main() {
var a int = 0
SetValue(&a) // 既使用指针
fmt.Println(a)
}
func SetValue(v interface{}) {
elem := reflect.ValueOf(v).Elem() // 又使用Elem()
elem.SetInt(666);
}
// 运行成功
666
Elem()跟指针的解引用是比较相似的,比如:
// 定义了一个*int类型,是一个指针类型
var a *int = new(int)
// 想要给a赋值,就要解引用再赋值
*a = 10
// val为一个Value类型,里面保存了a的指针
val := reflect.ValueOf(&a)
// 想要给val中的a赋值,需要使用Elem(),相当于解引用了
val.Elem().SetInt(100)
2、案例,通过结构体的tag给结构体赋值
比如当我们要写一个通用的读取配置文件的库,将读取到的配置文件中的配置赋值到一个结构体中时,就可以使用反射。在json包中就使用到了反射,以及一个操作数据库的一些框架中也用到了反射,传入一个结构体,框架就能将从数据库中读出的数据赋值给对应的字段。
注意:结构体的字段需要要首字母大写,因为如果不在同一个包中,就访问不到结构体中的字段。
比如有下面一个配置文件,需要解析配置文件并将配置赋值给对应结构体的字段:
[server]
host = "0.0.0.0"
port = 6387
name = "myserver"
version = "sv 1.0"
max_conn = 1024
max_package_size = 1024
worker_pool_size = 8
max_worker_task_len = 1024
time_out = 120
[mysql]
data_source_name = "root:1234569@(192.168.226.128:3306)/test?charset=utf8mb4&parseTime=true&loc=Local"
max_open_conns = 20
max_idle_conns = 10
[redis]
addr = "192.168.226.128:6379"
pool_size = 10
min_idle_conns = 5
password = ""
db = 0
对于每一个作用域,比如server算一个作用域、mysql算一个、redis算一个。对于每一个作用域我们都定义一个结构体:
为了方便赋值,需要给结构体的每个字段都加一个tag。
type ServerConf struct {
Host string `conf:"host"`
Port int `conf:"port"`
Name string `conf:"name"`
Version string `conf:"version"`
MaxConn int `conf:"max_conn"`
MaxPackageSize uint32 `conf:"max_package_size"`
WorkerPoolSize uint32 `conf:"worker_pool_size"`
MaxWorkerTaskLen uint32 `conf:"max_worker_task_len"`
TimeOut uint32 `conf:"time_out"`
}
type MysqlConf struct{
DataSourceName string `conf:"data_source_name"`
MaxOpenConns uint32 `conf:"max_open_conns"`
MaxIdleConns uint32 `conf:"max_idle_conns"`
}
type redisConf struct{
Addr string `conf:"addr"`
Password string `conf:"password"`
Db int `conf:"db"`
PoolSize int `conf:"pool_size"`
MinIdleConns int `conf:"min_idle_conns"`
}
为了解析上面的配置文件,可以定义一个函数Decode(filepath, field string, value interface{}) error来解析配置文件并将配置赋值给结构体。filepath为文件的路径,field为指定的域,value为传入的结构体。
- 首先需要读取配置文件,找到指定的域;
- 然后,依次读取一行,直到读取到下一个域,或者读取到文件结尾,然后将键值对存入一个map中。
- 最后,通过反射给结构体赋值。
代码如下:
package confparser
import (
"bufio"
"errors"
"fmt"
"io"
"os"
"reflect"
"strconv"
"strings"
)
/*
* @Desc: This package is used to parse configuration files.
*/
func Decode(filepath, field string, value interface{}) error {
// 判断文件是否存在
if !fileExist(filepath) {
return fmt.Errorf("File %s not exist", filepath)
}
// 域为空就报错
if field == "" {
return errors.New("File format error")
}
// 解析配置文件
return decodeField(filepath, field, value)
}
func decodeField(filepath string, field string, value interface{}) error {
file, err := os.Open(filepath)
if err != nil {
return err
}
defer file.Close()
reader := bufio.NewReader(file)
// 首先找到指定的域
for {
line, err := reader.ReadString('\n')
if err != nil || io.EOF == err {
break
}
s := trimnr(line)
l := strings.TrimSpace(s)
if len(l) < 1 {
continue
}
if l[0] != '[' {
continue
}
index := strings.Index(l, "]")
if index == -1 {
panic("File format error, line 53")
}
if l[1:index] != field {
continue
} else {
break
}
}
m := make(map[string]string)
// 依次读取域中的每一行并解析
for {
line, err := reader.ReadString('\n')
if err != nil || io.EOF == err {
break
}
l := strings.TrimSpace(line)
// 忽略空行
if len(l) < 1 {
continue
}
// 忽略注释
if l[0] == '#' {
continue
}
if l[0] == '[' {
break
}
// 可能在配置行的后面也有注释
index := strings.LastIndex(l, "#")
if index != -1 { // 后面有注释, 忽略注释
l = l[:index]
}
index = strings.Index(l, "=")
if index == -1 {
continue
}
k := strings.TrimSpace(l[:index])
if index + 1 >= len(l) {
continue
}
v := strings.TrimSpace(l[index + 1:])
m[k] = v
}
return reflectParseValue(m, value)
}
// 通过反射给结构体赋值
func reflectParseValue(m map[string]string, value interface{}) error {
if value == nil {
return errors.New("Value can't be nil.")
}
val := reflect.ValueOf(value).Elem()
for i := 0; i< val.NumField(); i++ {
fieldInfo := val.Type().Field(i)
tag := fieldInfo.Tag
stag := tag.Get("conf")
if stag == "" {
continue
}
if confVal, ok := m[stag]; ok {
f := val.Field(i)
switch f.Kind() {
case reflect.Uint64:
n, err := strconv.ParseUint(confVal, 10, 64)
if err != nil {
break
}
f.Set(reflect.ValueOf(n))
case reflect.Uint32:
n, err := strconv.Atoi(confVal)
if err != nil {
break
}
f.Set(reflect.ValueOf(uint32(n)))
case reflect.Uint16:
n, err := strconv.Atoi(confVal)
if err != nil {
break
}
f.Set(reflect.ValueOf(uint16(n)))
case reflect.Uint8:
n, err := strconv.Atoi(confVal)
if err != nil {
break
}
f.Set(reflect.ValueOf(uint8(n)))
case reflect.Int:
n, err := strconv.Atoi(confVal)
if err != nil {
break
}
f.Set(reflect.ValueOf(n))
case reflect.Bool:
b, err := strconv.ParseBool(confVal)
if err != nil {
break
}
f.Set(reflect.ValueOf(b))
case reflect.String:
l := len(confVal) - 1
if confVal[0] == '"' && confVal[l] == '"' {
confVal = confVal[1:l]
}
f.Set(reflect.ValueOf(confVal))
}
}
}
return nil
}
// 去除每行后的\r\n
func trimnr(str string) string {
l := len(str)
if l < 1 {
return str
}
l--
for {
if str[l] == '\n' || str[l] == '\r' {
l--
if l < 0 {
return ""
}
} else {
break
}
}
return str[:l + 1]
}
// 判断文件是否存在
func fileExist(path string) bool {
_, err := os.Stat(path)
if err != nil{
if os.IsExist(err){
return true
}
if os.IsNotExist(err){
return false
}
fmt.Println(err)
return false
}
return true
}
上面这个解析配置文件的方法就比较通用一些,只要符合配置文件的格式,我们可以定义一个结构体,在结构体中加入标签,然后调用Decode方法传入配置文件路径、作用域以及结构体变量指针就可以成功将配置文件解析到结构体中。
使用反射也可以调用结构体绑定的方法,通过Value.Method(i int)可以获取指定索引的方法,然后通过Call来调用。注意:方法的索引与方法定义的位置是无关的,它是根据方法名的Ascil码来排序的。