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

反射

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

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

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

javascript 复制代码
function showInfo(obj){
  if(typeof obj.info == "string"){
     console.log(obj.info) 
  }
  else {
    console.log('error: info not exist')
  }
}
showInfo({})
showInfo({info: "我是帅哥"})

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获取标签值。

go 复制代码
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())
	}
}

获取结构体所有属性信息

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

大致流程是这样的:

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

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

go 复制代码
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())
	}
}

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

前面两个工具函数的主要功能是获取结构体的信息,包括属性信息、结构体标签等等。这些信息我们是从哪获得的呢,是从结构体的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错误,导致程序运行中断。

go 复制代码
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())
	}

}
相关推荐
2401_854391084 分钟前
城镇住房保障:SpringBoot系统功能概览
java·spring boot·后端
hummhumm6 分钟前
Oracle 第29章:Oracle数据库未来展望
java·开发语言·数据库·python·sql·oracle·database
陈随易9 分钟前
兔小巢收费引发的论坛调研Node和Deno有感
前端·后端·程序员
聪明的墨菲特i14 分钟前
Django前后端分离基本流程
后端·python·django·web3
wainyz15 分钟前
Java NIO操作
java·开发语言·nio
喵叔哟23 分钟前
重构代码之用委托替代继承
开发语言·重构
lzb_kkk29 分钟前
【JavaEE】JUC的常见类
java·开发语言·java-ee
SEEONTIME29 分钟前
python-24-一篇文章彻底掌握Python HTTP库Requests
开发语言·python·http·http库requests
起名字真南1 小时前
【OJ题解】C++实现字符串大数相乘:无BigInteger库的字符串乘积解决方案
开发语言·c++·leetcode
tyler_download1 小时前
golang 实现比特币内核:实现基于椭圆曲线的数字签名和验证
开发语言·数据库·golang