• go反射特性实践——获取结构体标签的值、获取结构体属性信息、获取和修改结构体属性值


    反射

    首先,什么是反射呢,网上的概念和定义一大堆,我想说说自己的理解。在我看来,反射在程序上的体现就是,程序在运行的过程中能够对一个未知数据类型的变量进行信息获取和操作,这就是反射。

    在go语言里,未知数据类型那就是interface{}呗,有时候也叫做any。假如现在有个any类型的变量x,如果没有反射的话,那么除了把x赋值给另外一个any类型的变量,我们什么也做不了,因为any类型在语法层面上是不包含任何成员变量和成员方法的。但如果有反射的话,我们就可以访问到x的各种信息比如字段名称、数据类型、结构体标签,同时也可以对x进行一系列操作比如字段赋值、执行成员方法等等。这就是反射在编程时最直观的体现,我想应该这样解释应该比那些枯燥的、学术化的概念更容易理解一些。

    反射的概念只存在于强类型的语言,因为弱类型的语言在编译时不会对类型进行检查,不使用显式的反射技术就可以对any类型数据进行读取和操作,比如下面这段demo是用JavaScript写的,JavaScript是一门典型的弱类型语言。对于showInfo方法来说,它接收一个any类型的obj对象,我们不需要在代码上特意去进行反射操作,就已经可以访问obj对象的属性值了。所以我说,弱类型语言没有反射的概念,或者说弱类型语言本身就处处充满了反射。

    function showInfo(obj){
      if(typeof obj.info == "string"){
         console.log(obj.info) 
      }
      else {
        console.log('error: info not exist')
      }
    }
    showInfo({})
    showInfo({info: "我是帅哥"})
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    go反射包的官方文档在这里reflect,在本篇文章中,我将使用go的反射技术,封装一些工具函数,结合实际例子来展示反射的使用方法。这里有反射基础操作的教程,我感觉写的很不错

    获取结构体标签的值

    代码如下面所示,通过方法getStructTag(subject interface{}, fieldName, tagName string) (string, error),往方法中传入结构体或结构体指针、属性名称、标签key值,然后可以获得该结构体的该属性的该标签值。函数的执行结果一共有如下几个情况:

    1. 传入的参数是结构体或结构体指针,属性和标签都存在,则返回对应的标签值。
    2. 传入的参数是结构体或结构体指针,属性存在但标签不存在,则返回空字符串。
    3. 传入的参数是结构体或结构体指针,属性不存在,则返回error
    4. 传入的参数是nil,返回error
    5. 传入的参数不是结构体也不是结构体指针,则返回error

    代码注释已经写的很清楚了,代码逻辑也比较简单,主要流程就是通过reflect.TypeOf()方法获取结构体的Type,如果传入的是结构体指针的话则通过Elem()方法获取结构体Type,然后通过FieldByName()方法根据属性名获取属性相关信息,最后通过Tag获取标签值。

    package main
    
    import (
    	"errors"
    	"fmt"
    	"reflect"
    )
    
    // User 随便声明一个User结构体,用于测试
    type User struct {
    	Name string `json:"name" gorm:"user_name"`
    	Age  int    `json:"age" gorm:"user_age"`
    }
    
    // getStructTag 获取结构体标签内容,subject参数为目标结构体,fieldName为属性名,tagName为结构体标签的键值
    func getStructTag(subject interface{}, fieldName, tagName string) (string, error) {
    	if subject == nil {
    		return "", errors.New("error: subject can not be nil")
    	}
    
    	typeOfSubject := reflect.TypeOf(subject)
    	
    	//switch里面判断subject的类型,如果是结构体指针类型则做一系列转换,获取结构体类型
    	switch typeOfSubject.Kind() {
    	case reflect.Struct:
    		break
    	case reflect.Ptr: //如果是指针类型,则需要通过Elem()函数得到它的实际数据类型
    		for typeOfSubject.Kind() == reflect.Ptr {
    			typeOfSubject = typeOfSubject.Elem()
    		}
    		if typeOfSubject.Kind() != reflect.Struct { //如果实际数据类型不是结构体类型,则返回错误
    			return "", errors.New("error: subject can not be " + typeOfSubject.Kind().String())
    		}
    	default: //如果不是结构体类型也不是指针类型,则返回错误
    		return "", errors.New("error: subject can not be " + typeOfSubject.Kind().String())
    	}
    
    	if field, ok := typeOfSubject.FieldByName(fieldName); ok {
    		return field.Tag.Get(tagName), nil
    	} else {
    		return "", errors.New("error: subject doesn't has the field: " + fieldName)
    	}
    }
    // 下面的都是测试内容
    // 测试编写的getStructTag函数是否能正确执行
    func main() {
    
    	// 测试样例1,控制台输出 user_name
    	if tagValue, err := getStructTag(User{}, "Name", "gorm"); err == nil {
    		fmt.Println(tagValue)
    	} else {
    		fmt.Println(err.Error())
    	}
    
    	// 测试样例2,控制台输出 age
    	if tagValue, err := getStructTag(User{}, "Age", "json"); err == nil {
    		fmt.Println(tagValue)
    	} else {
    		fmt.Println(err.Error())
    	}
    
    	// 测试样例3,控制台输出 空字符串
    	if tagValue, err := getStructTag(User{}, "Name", "1111"); err == nil {
    		fmt.Println(tagValue)
    	} else {
    		fmt.Println(err.Error())
    	}
    
    	// 测试样例4,控制台输出 error: subject doesn't has the field: daluan
    	if tagValue, err := getStructTag(User{}, "daluan", "1111"); err == nil {
    		fmt.Println(tagValue)
    	} else {
    		fmt.Println(err.Error())
    	}
    
    	// 测试样例5,控制台输出 error: subject can not be nil
    	if tagValue, err := getStructTag(nil, "Name", "name"); err == nil {
    		fmt.Println(tagValue)
    	} else {
    		fmt.Println(err.Error())
    	}
    
    	// 测试样例6,控制台输出 subject can not be int
    	if tagValue, err := getStructTag(1, "Name", "name"); err == nil {
    		fmt.Println(tagValue)
    	} else {
    		fmt.Println(err.Error())
    	}
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90

    获取结构体所有属性信息

    getFields(subject interface{}) ([]reflect.StructField, error)方法可以根据传入的结构体或结构体指针,返回该结构体所有属性的信息。
    大致流程是这样的:

    • 使用TypeOf()函数获取数据类型,如果是指针则使用Elem()函数做一下转换
    • 使用NumField()函数获取属性个数,使用Field()函数根据属性下标获取StructField类型的属性信息,
    • return返回结果即可

    代码逻辑不复杂,注释写的比较清楚,结合下面的代码应该很容易理解。

    package main
    
    import (
    	"errors"
    	"fmt"
    	"reflect"
    )
    
    // User 随便声明一个User结构体,用于测试
    type User struct {
    	Name     string `json:"name" gorm:"user_name"`
    	Age      int    `json:"age" gorm:"user_age"`
    	password string
    	Friends  []*User `json:"friends" gorm:"-"`
    }
    
    func getFields(subject interface{}) ([]reflect.StructField, error) {
    	fields := make([]reflect.StructField, 0)
    
    	typeOfSubject := reflect.TypeOf(subject)
    
    	//switch里面判断subject的类型,如果是结构体指针类型则做一系列转换,获取结构体类型
    	switch typeOfSubject.Kind() {
    	case reflect.Struct:
    		break
    	case reflect.Ptr: //如果是指针类型,则需要通过Elem()函数得到它的实际数据类型
    		for typeOfSubject.Kind() == reflect.Ptr {
    			typeOfSubject = typeOfSubject.Elem()
    		}
    		if typeOfSubject.Kind() != reflect.Struct { //如果实际数据类型不是结构体类型,则返回错误
    			return fields, errors.New("error: subject can not be " + typeOfSubject.Kind().String())
    		}
    	default: //如果不是结构体类型也不是指针类型,则返回错误
    		return fields, errors.New("error: subject can not be " + typeOfSubject.Kind().String())
    	}
    
    	for i := 0; i < typeOfSubject.NumField(); i++ {
    		fields = append(fields, typeOfSubject.Field(i))
    	}
    	return fields, nil
    }
    
    // 测试函数
    func main() {
    	// 测试样例1,输出内容如下:
    	// 属性名: Name, 数据类型名称: string
    	// 属性名: Age, 数据类型名称: int
    	// 属性名: password, 数据类型名称: string
    	// 属性名: Friends, 数据类型名称: []*main.User
    	if fields, err := getFields(&User{}); err == nil {
    		for _, f := range fields {
    			fmt.Printf("属性名: %s, 数据类型名称: %s\n", f.Name, f.Type.String())
    		}
    	} else {
    		fmt.Println(err.Error())
    	}
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58

    获取和修改结构体属性的值

    前面两个工具函数的主要功能是获取结构体的信息,包括属性信息、结构体标签等等。这些信息我们是从哪获得的呢,是从结构体的Type里面获得的,我先通过reflect.TypeOf()方法获得结构体的Type,然后就能获得一些静态信息,也就是在编写代码时就定义好的信息。

    和前面不同的是,本节中的工具函数的功能是获取和修改结构体的值,这个值不是在编写代码时就确定好的,而是随着程序的运行而动态变换的,这时候我们需要使用的就是结构体的Value,我们先通过reflect.ValueOf()获取结构体的Value对象,然后通过Value对象来获取和修改结构体的属性值。具体代码如下面所示,代码注释写的很清楚,代码逻辑也很简单。

    getFieldValue(subject interface{}, fieldName string) (interface{}, error)函数的功能是获取传入的结构体结构体指针的某个属性值,并返回。注意:如果该属性不存在,则返回nil值和error

    modifyField(subject interface{}, fieldName string, fieldValue interface{}) error函数的功能是修改传入的结构体指针的属性值,fieldName是要修改的属性名称,fieldValue是修改后的值。注意,传入的fieldValue数据类型必须和要修改的结构体属性的数据类型保持一致,否则会发生panic错误,导致程序运行中断。

    package main
    
    import (
    	"errors"
    	"fmt"
    	"reflect"
    )
    
    // Person Car 随便声明两个具有嵌套关系的结构体,用于测试
    type Person struct {
    	Name        string
    	age         int
    	PersonalCar *Car
    }
    
    func (p *Person) showInfo() {
    	fmt.Printf("my name is %s, I am %d years old. My car is %s and worth %d$\n",
    		p.Name, p.age, p.PersonalCar.Brand, p.PersonalCar.Price)
    }
    
    type Car struct {
    	Brand string
    	Price int
    }
    
    // 获取结构体或结构体指针subject的fieldName属性值
    func getFieldValue(subject interface{}, fieldName string) (interface{}, error) {
    	valueOfSubject := reflect.ValueOf(subject)
    
    	// 老规矩,还是先判断一下传入参数的数据类型,如果是指针则进行取值处理
    	if valueOfSubject.Kind() == reflect.Ptr {
    		for valueOfSubject.Kind() == reflect.Ptr {
    			valueOfSubject = valueOfSubject.Elem()
    		}
    	}
    	if valueOfSubject.Kind() != reflect.Struct {
    		return nil, errors.New("subject is not a pointer of struct or struct")
    	}
    
    	// 如果该属性存在的话,field不是零值
    	if field := valueOfSubject.FieldByName(fieldName); !field.IsZero() {
    		return field.Interface(), nil
    	} else {
    		// 如果属性不存在,则直接返回错误
    		return nil, errors.New("field: " + fieldName + " not exist in subject")
    	}
    
    }
    
    // 把结构体指针subject里的fieldName属性修改为fieldValue,注意:如果fieldValue的数据类型与结构体属性不匹配的话,会panic
    func modifyField(subject interface{}, fieldName string, fieldValue interface{}) error {
    
    	valueOfSubject := reflect.ValueOf(subject)
    
    	// 老规矩,还是先判断一下传入参数的数据类型,如果是指针则进行取值处理
    	if valueOfSubject.Kind() == reflect.Ptr {
    		for valueOfSubject.Kind() == reflect.Ptr {
    			valueOfSubject = valueOfSubject.Elem()
    		}
    	} else {
    		return errors.New("subject is not a pointer")
    	}
    	if valueOfSubject.Kind() != reflect.Struct {
    		return errors.New("subject is not a pointer of struct")
    	}
    
    	// 先获取要修改的属性,如果该属性存在的话,field不是零值
    	if field := valueOfSubject.FieldByName(fieldName); !field.IsZero() {
    		if field.CanSet() {
    			// 有一些属性是不能够从外部修改的,比如私有属性,所以先判断一下能不能修改
    			field.Set(reflect.ValueOf(fieldValue))
    			return nil
    		} else {
    			return errors.New("field: " + fieldName + " in subject can not be set")
    		}
    
    	} else {
    		// 如果属性不存在,则直接返回错误
    		return errors.New("field: " + fieldName + " not exist in subject")
    	}
    }
    
    // 测试函数
    // 以下均为测试内容,测试modifyField、getFieldValue函数功能是否正常
    func main() {
    	p := Person{
    		Name: "张三",
    		age:  18,
    		PersonalCar: &Car{
    			Brand: "奥迪",
    			Price: 500,
    		},
    	}
    
    	// 成功修改属性Name,控制台输出:my name is 李四, I am 18 years old. My car is 奥迪 and worth 500$
    	if err := modifyField(&p, "Name", "李四"); err == nil {
    		p.showInfo()
    	} else {
    		fmt.Println(err.Error())
    	}
    
    	// 不能修改age属性,因为age是私有属性,控制台输出:field: age in subject can not be set
    	if err := modifyField(&p, "age", 99); err == nil {
    		p.showInfo()
    	} else {
    		fmt.Println(err.Error())
    	}
    
    	// 能正常修改PersonalCar属性,控制台输出:my name is 李四, I am 18 years old. My car is 五菱 and worth 900$
    	if err := modifyField(&p, "PersonalCar", &Car{Brand: "五菱", Price: 900}); err == nil {
    		p.showInfo()
    	} else {
    		fmt.Println(err.Error())
    	}
    
    	// 能正确获取PersonalCar属性,控制台输出 &{五菱 900}
    	if personCar, err := getFieldValue(p, "PersonalCar"); err == nil {
    		fmt.Println(personCar)
    	} else {
    		fmt.Println(err.Error())
    	}
    
    	// "car"是字符串变量,不能赋值给*Car类型的PersonalCar,会出现panic
    	if err := modifyField(&p, "PersonalCar", "car"); err == nil {
    		p.showInfo()
    	} else {
    		fmt.Println(err.Error())
    	}
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
  • 相关阅读:
    CMAK Kafka可视化管理工具
    数字孪生技术在光网络中的应用与问题
    Linux上Docker的安装以及作为非运维人员应当掌握哪些Docker命令
    JavaScript事件触发
    vue学习(基础下)
    【LeetCode】【剑指offer】【圆圈中最后剩下的数字】
    线程同步之信号量
    【FFMPEG】解决截取MP4视频的中间段时,截取完成后前几帧视频卡住,但是有声音的情况
    第十三届蓝桥杯大赛软件赛决赛(Java 大学C组)
    索引位图制作、C++读写流程
  • 原文地址:https://blog.csdn.net/m0_52744273/article/details/132776734