searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享
原创

go语言reflect包实践

2023-07-07 06:20:58
8
0

go语言reflect包实践

1. 反射基本概念

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

golang中的接口值是两字节的数据结构,两个字节各是一个指针,其中:

  • 第一个指针指向一个叫做iTable的内部表,表中包含两方面内容,一是值的类型信息,二是值的方法集

  • 第二个指针指向实际存储的值。

对应地,任意接口值在go反射中都分为reflect.Type和reflect.Value两部分,我们可分别通过reflect.TypeOf()和reflect.ValueOf()函数对象的Type和Value。

本文将使用reflect包对结构体进行遍历、赋值与方法调用操作,以熟悉了解reflect包的基本概念与使用。

2. struct字段的遍历

首先定义一个如下的简单结构体,如程序所示,共有三个字段。

 type Employee struct {
  Name   string
  Role   string
  Salary float64
 }

下面,我们尝试用reflect包对一个Employee类型的值进行遍历,要求输出字段的名称、类型和值。

 var xiaowang = &Employee{
  Name:   "xiaowang",
  Role:   "glory engineer",
  Salary: 0.5,
 }
 
 func traverse(target interface{}) {
  sVal := reflect.ValueOf(target)
  sType := reflect.TypeOf(target)
  if sType.Kind() == reflect.Ptr {
        //用Elem()获得实际的value
  sVal = sVal.Elem()
  sType = sType.Elem()
  }
  num := sVal.NumField()
  for i := 0; i < num; i++ {
  f := sType.Field(i)
  val := sVal.Field(i).Interface()
  fmt.Printf("%5s %v = %v\n", f.Name, f.Type, val)
  }
 }
 
 func main() {
  traverse(xiaowang)
 }

需要注意的是,程序在正式遍历字段前,对种类(Kind)为指针(reflect.Ptr)的值调用了Elem()方法,令其指向实际的值。

(而事实上,reflect.Value.NumField()与reflect.Value.Field()等方法均需要调用者的种类(Kind)为结构体(reflect.Struct),否则程序会panic。)

运行程序,输出如下,可见已成功遍历了结构体的各字段

 Name string = xiaowang
 Role string = glory engineer
 Salary float64 = 0.5

3. struct赋值操作(根据map构建新struct)

给定一个map值,我们根据该map提供的信息,恢复构建出一个Employee类型的值

 var employeeData = map[string]interface{}{
  "name":   "laozhang",
  "role":   "annother glory engineer",
  "salary": 1.5,
 }

针对employeeData中key与结构体中字段名大小写不一致的问题,我们在Employee结构体定义中,给字段加入一些tag信息。(由于map信息往往由外部给出,其key不一定满足go的字段命名习惯,故直接修改字段名的方法来达到两者一致是不合适的。)

 type Employee struct {
  Name   string `key:"name"`
  Role   string `key:"role"`
  Salary float64 `key:"salary"`
 }

在对结构体对象赋值过程中,需要注意两方面的内容:

  1. 用reflect.Value.Set()方法给对应字段赋值,注意该方法的传入参数是reflect.Value类型的

  2. 在给字段赋值前需要进行类型检查,若map中的value和字段类型一致,则可以直接调用Set()方法赋值;若两者类型不一致,则需调用reflect.Type.ConvertibleTo()方法来判断是否可以进行类型转换,若可转换则调用reflect.Value.Convert()方法转换传参类型,倘若不可转换而强行调用Convert()方法,会导致程序panic。为了简便起见,若类型不可转换,我们在程序中同样返回panic并给出错误信息。

 func rebuiltStruct(mapData map[string]interface{}, target interface{}) {
  sVal := reflect.ValueOf(target)
  sType := reflect.TypeOf(target)
  if sType.Kind() == reflect.Ptr {
  sVal = sVal.Elem()
  sType = sType.Elem()
  }
  num := sVal.NumField()
  for i := 0; i < num; i++ {
  f := sType.Field(i)
  val := sVal.Field(i)
  key := f.Tag.Get("key")
  if dataVal, ok := mapData[key]; ok {
  //类型判断与转换
  dataType := reflect.TypeOf(dataVal)
  fieldType := val.Type()
  if dataType == fieldType {
  val.Set(reflect.ValueOf(dataVal))
  } else {
  if dataType.ConvertibleTo(fieldType) {
  val.Set(reflect.ValueOf(dataVal).Convert(fieldType))
  } else {
                    panic(fmt.Sprintf("failed to convert from %s to %s \n", dataType, fieldType))
  }
  }
  } else {
  fmt.Printf("key %s not found in struct definition! \n", key)
  }
  }
  traverse2(target)
 }
 func main() {
  rebuiltStruct(employeeData, &Employee{})
 }

运行上述程序,可得输出如下

 Name string = laozhang
 Role string = annother glory engineer
 Salary float64 = 1.5

4. 调用struct的方法

首先,我们对Employee结构体增加方法:

 func (e Employee) Work(i int) {
  fmt.Printf("I work for %v hours per day. \n", i)
 }

下面,通过指定方法名的方法调用它

 func callByMethodName(x interface{}) {
  t := reflect.TypeOf(x).Elem()
  v := reflect.ValueOf(x).Elem()
  if t.Kind() == reflect.Ptr {
  v = v.Elem()
  t = t.Elem()
  }
 
  //通过方法名调用指定方法,这里同样无法调用未导出的方法
    printMethod := v.MethodByName("Work")
     
  //此处需注意判断Zero Value
  if printMethod.IsValid() {
  args := []reflect.Value{reflect.ValueOf(10)}
  printMethod.Call(args)
  } else {
  fmt.Printf("method not found!")
  }
 }

参考链接

0条评论
作者已关闭评论
l****n
3文章数
0粉丝数
l****n
3 文章 | 0 粉丝
l****n
3文章数
0粉丝数
l****n
3 文章 | 0 粉丝
原创

go语言reflect包实践

2023-07-07 06:20:58
8
0

go语言reflect包实践

1. 反射基本概念

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

golang中的接口值是两字节的数据结构,两个字节各是一个指针,其中:

  • 第一个指针指向一个叫做iTable的内部表,表中包含两方面内容,一是值的类型信息,二是值的方法集

  • 第二个指针指向实际存储的值。

对应地,任意接口值在go反射中都分为reflect.Type和reflect.Value两部分,我们可分别通过reflect.TypeOf()和reflect.ValueOf()函数对象的Type和Value。

本文将使用reflect包对结构体进行遍历、赋值与方法调用操作,以熟悉了解reflect包的基本概念与使用。

2. struct字段的遍历

首先定义一个如下的简单结构体,如程序所示,共有三个字段。

 type Employee struct {
  Name   string
  Role   string
  Salary float64
 }

下面,我们尝试用reflect包对一个Employee类型的值进行遍历,要求输出字段的名称、类型和值。

 var xiaowang = &Employee{
  Name:   "xiaowang",
  Role:   "glory engineer",
  Salary: 0.5,
 }
 
 func traverse(target interface{}) {
  sVal := reflect.ValueOf(target)
  sType := reflect.TypeOf(target)
  if sType.Kind() == reflect.Ptr {
        //用Elem()获得实际的value
  sVal = sVal.Elem()
  sType = sType.Elem()
  }
  num := sVal.NumField()
  for i := 0; i < num; i++ {
  f := sType.Field(i)
  val := sVal.Field(i).Interface()
  fmt.Printf("%5s %v = %v\n", f.Name, f.Type, val)
  }
 }
 
 func main() {
  traverse(xiaowang)
 }

需要注意的是,程序在正式遍历字段前,对种类(Kind)为指针(reflect.Ptr)的值调用了Elem()方法,令其指向实际的值。

(而事实上,reflect.Value.NumField()与reflect.Value.Field()等方法均需要调用者的种类(Kind)为结构体(reflect.Struct),否则程序会panic。)

运行程序,输出如下,可见已成功遍历了结构体的各字段

 Name string = xiaowang
 Role string = glory engineer
 Salary float64 = 0.5

3. struct赋值操作(根据map构建新struct)

给定一个map值,我们根据该map提供的信息,恢复构建出一个Employee类型的值

 var employeeData = map[string]interface{}{
  "name":   "laozhang",
  "role":   "annother glory engineer",
  "salary": 1.5,
 }

针对employeeData中key与结构体中字段名大小写不一致的问题,我们在Employee结构体定义中,给字段加入一些tag信息。(由于map信息往往由外部给出,其key不一定满足go的字段命名习惯,故直接修改字段名的方法来达到两者一致是不合适的。)

 type Employee struct {
  Name   string `key:"name"`
  Role   string `key:"role"`
  Salary float64 `key:"salary"`
 }

在对结构体对象赋值过程中,需要注意两方面的内容:

  1. 用reflect.Value.Set()方法给对应字段赋值,注意该方法的传入参数是reflect.Value类型的

  2. 在给字段赋值前需要进行类型检查,若map中的value和字段类型一致,则可以直接调用Set()方法赋值;若两者类型不一致,则需调用reflect.Type.ConvertibleTo()方法来判断是否可以进行类型转换,若可转换则调用reflect.Value.Convert()方法转换传参类型,倘若不可转换而强行调用Convert()方法,会导致程序panic。为了简便起见,若类型不可转换,我们在程序中同样返回panic并给出错误信息。

 func rebuiltStruct(mapData map[string]interface{}, target interface{}) {
  sVal := reflect.ValueOf(target)
  sType := reflect.TypeOf(target)
  if sType.Kind() == reflect.Ptr {
  sVal = sVal.Elem()
  sType = sType.Elem()
  }
  num := sVal.NumField()
  for i := 0; i < num; i++ {
  f := sType.Field(i)
  val := sVal.Field(i)
  key := f.Tag.Get("key")
  if dataVal, ok := mapData[key]; ok {
  //类型判断与转换
  dataType := reflect.TypeOf(dataVal)
  fieldType := val.Type()
  if dataType == fieldType {
  val.Set(reflect.ValueOf(dataVal))
  } else {
  if dataType.ConvertibleTo(fieldType) {
  val.Set(reflect.ValueOf(dataVal).Convert(fieldType))
  } else {
                    panic(fmt.Sprintf("failed to convert from %s to %s \n", dataType, fieldType))
  }
  }
  } else {
  fmt.Printf("key %s not found in struct definition! \n", key)
  }
  }
  traverse2(target)
 }
 func main() {
  rebuiltStruct(employeeData, &Employee{})
 }

运行上述程序,可得输出如下

 Name string = laozhang
 Role string = annother glory engineer
 Salary float64 = 1.5

4. 调用struct的方法

首先,我们对Employee结构体增加方法:

 func (e Employee) Work(i int) {
  fmt.Printf("I work for %v hours per day. \n", i)
 }

下面,通过指定方法名的方法调用它

 func callByMethodName(x interface{}) {
  t := reflect.TypeOf(x).Elem()
  v := reflect.ValueOf(x).Elem()
  if t.Kind() == reflect.Ptr {
  v = v.Elem()
  t = t.Elem()
  }
 
  //通过方法名调用指定方法,这里同样无法调用未导出的方法
    printMethod := v.MethodByName("Work")
     
  //此处需注意判断Zero Value
  if printMethod.IsValid() {
  args := []reflect.Value{reflect.ValueOf(10)}
  printMethod.Call(args)
  } else {
  fmt.Printf("method not found!")
  }
 }

参考链接

文章来自个人专栏
文章 | 订阅
0条评论
作者已关闭评论
作者已关闭评论
0
0