经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 程序设计 » Go语言 » 查看文章
go语言reflect包最佳实践之struct操作(遍历、赋值与方法调用)
来源:cnblogs  作者:lwjj  时间:2020/11/9 16:09:45  对本文有异议

go语言reflect包最佳实践之struct操作(遍历、赋值与方法调用)

1. 反射基本概念

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

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

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

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

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

2. struct字段的遍历

2.1 简单结构体的遍历

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

  1. type Employee struct {
  2. Name string
  3. Role string
  4. Salary float64
  5. }

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

  1. var xiaowang = &Employee{
  2. Name: "xiaowang",
  3. Role: "glory engineer",
  4. Salary: 0.5,
  5. }
  6. func traverse(target interface{}) {
  7. sVal := reflect.ValueOf(target)
  8. sType := reflect.TypeOf(target)
  9. if sType.Kind() == reflect.Ptr {
  10. //用Elem()获得实际的value
  11. sVal = sVal.Elem()
  12. sType = sType.Elem()
  13. }
  14. num := sVal.NumField()
  15. for i := 0; i < num; i++ {
  16. f := sType.Field(i)
  17. val := sVal.Field(i).Interface()
  18. fmt.Printf("%5s %v = %v\n", f.Name, f.Type, val)
  19. }
  20. }
  21. func main() {
  22. traverse(xiaowang)
  23. }

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

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

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

  1. Name string = xiaowang
  2. Role string = glory engineer
  3. Salary float64 = 0.5

2.2 复杂结构体的遍历

考虑字段类型为结构体的特殊情况,比如

  1. type ComplexStruct struct {
  2. CField1 string
  3. CField2 *SimpleStruct
  4. CField3 []string
  5. }

我们需要将程序简单修改为递归遍历的结构,如下所示

  1. var ComplexTarget = &ComplexStruct{
  2. CField1: "CValue1",
  3. CField2: SimpleTarget,
  4. CField3: []string{"CValue3", "sadf"},
  5. }
  6. func traverse2(target interface{}) {
  7. sVal := reflect.ValueOf(target)
  8. sType := reflect.TypeOf(target)
  9. if sType.Kind() == reflect.Ptr {
  10. sVal = sVal.Elem()
  11. sType = sType.Elem()
  12. }
  13. num := sVal.NumField()
  14. for i := 0; i < num; i++ {
  15. //判断字段是否为结构体类型,或者是否为指向结构体的指针类型
  16. if sVal.Field(i).Kind() == reflect.Struct || (sVal.Field(i).Kind() == reflect.Ptr && sVal.Field(i).Elem().Kind() == reflect.Struct) {
  17. traverse2(sVal.Field(i).Interface())
  18. } else {
  19. f := sType.Field(i)
  20. val := sVal.Field(i).Interface()
  21. fmt.Printf("%5s %v = %v\n", f.Name, f.Type, val)
  22. }
  23. }
  24. }
  25. func main() {
  26. traverse2(ComplexTarget)
  27. }

每次访问字段都先判断其是否为结构体类型,或者是否为指向结构体的指针类型,若是则递归调用递归方法,否则直接输出访问结果即可。

运行程序,可得如下输出,可见已实现了期望的递归遍历的功能。

  1. TeamName string = urTeam
  2. Name string = xiaowang
  3. Role string = glory engineer
  4. Salary float64 = 0.5
  5. Duty []string = [code debug]

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

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

  1. var employeeData = map[string]interface{}{
  2. "name": "laozhang",
  3. "role": "annother glory engineer",
  4. "salary": 1.5,
  5. }

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

  1. type Employee struct {
  2. Name string `key:"name"`
  3. Role string `key:"role"`
  4. Salary float64 `key:"salary"`
  5. }

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

  1. 用reflect.Value.Set()方法给对应字段赋值,注意该方法的传入参数是reflect.Value类型的
  2. 在给字段赋值前需要进行类型检查,若map中的value和字段类型一致,则可以直接调用Set()方法赋值;若两者类型不一致,则需调用reflect.Type.ConvertibleTo()方法来判断是否可以进行类型转换,若可转换则调用reflect.Value.Convert()方法转换传参类型,倘若不可转换而强行调用Convert()方法,会导致程序panic。为了简便起见,若类型不可转换,我们在程序中同样返回panic并给出错误信息。
  1. func rebuiltStruct(mapData map[string]interface{}, target interface{}) {
  2. sVal := reflect.ValueOf(target)
  3. sType := reflect.TypeOf(target)
  4. if sType.Kind() == reflect.Ptr {
  5. sVal = sVal.Elem()
  6. sType = sType.Elem()
  7. }
  8. num := sVal.NumField()
  9. for i := 0; i < num; i++ {
  10. f := sType.Field(i)
  11. val := sVal.Field(i)
  12. key := f.Tag.Get("key")
  13. if dataVal, ok := mapData[key]; ok {
  14. //类型判断与转换
  15. dataType := reflect.TypeOf(dataVal)
  16. fieldType := val.Type()
  17. if dataType == fieldType {
  18. val.Set(reflect.ValueOf(dataVal))
  19. } else {
  20. if dataType.ConvertibleTo(fieldType) {
  21. val.Set(reflect.ValueOf(dataVal).Convert(fieldType))
  22. } else {
  23. panic(fmt.Sprintf("failed to convert from %s to %s \n", dataType, fieldType))
  24. }
  25. }
  26. } else {
  27. fmt.Printf("key %s not found in struct definition! \n", key)
  28. }
  29. }
  30. traverse2(target)
  31. }
  32. func main() {
  33. rebuiltStruct(employeeData, &Employee{})
  34. }

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

  1. Name string = laozhang
  2. Role string = annother glory engineer
  3. Salary float64 = 1.5

4. 调用struct的方法

4.1 方法遍历与无参调用

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

  1. func (e Employee) Code() {
  2. fmt.Printf("I like to code \n")
  3. }
  4. func (e Employee) Debug() {
  5. fmt.Printf("I dislike to debug \n")
  6. }
  7. func (e Employee) raiseSalary() {
  8. fmt.Printf("I want to raise my salasy \n")
  9. }

接着我们编写程序,用reflect遍历调用两个方法:

  1. func callMethodWithReflect(x interface{}) {
  2. t := reflect.TypeOf(x).Elem()
  3. v := reflect.ValueOf(x).Elem()
  4. if t.Kind() == reflect.Ptr {
  5. v = v.Elem()
  6. t = t.Elem()
  7. }
  8. //NumMethod()只会计算导出的方法,即首字母大写的方法
  9. numOfMethod := v.NumMethod()
  10. fmt.Printf("We have %v methods. \n", numOfMethod)
  11. //以索引的方式遍历调用所有的方法
  12. for i := 0; i < numOfMethod; i++ {
  13. methodName := t.Method(i).Name
  14. methodType := t.Method(i).Type
  15. //方法的Type也可以用v.Method(i).Type()获得
  16. fmt.Printf("method-%v name is %s \n", i, methodName)
  17. fmt.Printf("method-%v type is %s \n", i, methodType)
  18. args := []reflect.Value{} //不含参调用
  19. v.Method(i).Call(args)
  20. }
  21. }

运行程序可得如下结果,发现程序只调用了Code()和Debug(),忽略了raiseSalary()方法。这是因为reflect.Value.NumMethod()只能发现导出(首字母大写)的方法,而reflect.Type.Method()也只能访问到导出的方法。

需要注意的是,reflect.Type.Method()方法返回的仍是一个reflect.Value类型的值,只不过其Kind为Func。在调用方法时,reflect.Value.Call()方法的入参是一个reflect.Value的切片,且若Call()方法的调用者的Kind不是Func种类,程序会panic。

  1. We have 2 methods.
  2. method-0 name is Code
  3. method-0 type is func(main.Employee)
  4. I like to code
  5. method-1 name is Debug
  6. method-1 type is func(main.Employee)
  7. I dislike to debug

因此若修改方法raiseSalary()为导出方法,

  1. func (e Employee) RaiseSalary() {
  2. fmt.Printf("I want to raise my salary \n")
  3. }

再重新执行程序可以发现三个方法均已被遍历到

  1. We have 3 methods.
  2. ...
  3. ...
  4. method-2 name is RaiseSalary
  5. method-2 type is func(main.Employee)
  6. I want to raise my salary

4.2 按方法名调用

我们再增加一个含参数方法

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

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

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

这里需要注意以下容易踩坑的地方:

  1. reflect.Value.MethodByName()同样只能访问到导出了的方法,若传入了未导出的方法名,方法会返回一个零值(Zero Value),而不会报错
  2. 若直接对零值(Zero Value)调用Call()方法,程序会panic,故在调用Call()之前需验证返回值是否为零值。
  3. 验证一个值是否是零值(Zero Value),千万不可以用isZero()方法,否则程序会panic。这是因为零值不是IsZero方法返回true的值,而是IsValid方法返回false的值。在golang文档中,有如下说明:"The zero Value represents no value. Its IsValid method returns false, its Kind method returns Invalid, its String method returns "", and all other methods panic." 即,零值的IsValid方法返回false,其Kind方法返回Invalid,其String方法返回"",对零值的任何其他方法的调用均会导致panic

参考链接

原文链接:http://www.cnblogs.com/liweijiee/p/13879163.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号