反射是所有面向对象语言的一个重点,它为开发者提供了灵活的操作可能。利用反射可以获取不同对象/结构体的信息,制定不同的策略,实现复杂的操作。
go与java的反射操作思维大有不同,学习的时候也遇到过一些困难,记录下来加深印象。
任务:读取Excel文件,根据传入的结构体生成对应的对象。
数据准备
我准备的是之前学校发的学生信息(请不要纠结为什么我要用这个数据,这个就是我随便翻找到的)
根据Excel表中的字段信息,我们可以创建一个Student结构体,它包含了对应的字段:
// 学生结构体
type Student struct {
Sno int // 学号
Sname string // 姓名
Sclass string // 班级
Sbank string // 卡号
}
复制代码
获取结构体信息
对于入口函数的操作是这样的:
func main() {
var path string = "D:/GoCode/go-datafilter/reources/xxx.xlsx"
dataType := util.ReadDataFromExcel(path, &Student{})
for _, data := range dataType.Data {
fmt.Println(data)
}
}
复制代码
内容很简单,就是向ReadDataFromExcel方法里传入两个参数:
- filePath:要读取的Excel文件路径
- targetStruct:需要反射的结构体
在ReadDataFromExcel方法里,我们需要进行三个步骤:
- 获取结构体信息
- 获取Excel表信息
- 通过反射创建对象
首先把完整的获取结构体信息的代码贴出来,然后对此进行分析:
// 字段类型切片
var fieldType = make([]string, 0)
// 字段名称切片
var fieldName = make([]string, 0)
// 读取结构体字段的名字和类型
func getFieldNameAndType(typeof reflect.Type) reflect.Type {
if typeof.Kind() == reflect.Ptr {
typeof = typeof.Elem()
}
if typeof.Kind() != reflect.Struct {
panic("bab type")
}
fieldsNum := typeof.NumField()
for i := 0; i < fieldsNum; i++ {
fieldType = append(fieldType, typeof.Field(i).Type.Name())
fieldName = append(fieldName, typeof.Field(i).Name)
}
datatype.FieldName = fieldName
datatype.FieldType = fieldType
return typeof
}
复制代码
在Go语言中,它提供了reflect包用来辅助我们进行反射操作,该包封装了多个方法。
Go中对于被反射对象的分类有两种方法,一种是根据类型Type,一种是根据种类Kind。Kind是比Type更广泛的概念,Type指的是系统原生数据类型,如int、string、bool等类型,以及使用type关键字定义的类型,而Kind是类型归属的品种。如下例所示:
func main() { student := Student{} typeOf := reflect.TypeOf(student) fmt.Println(typeOf.Kind()) // struct:它是一个结构体种类 // Name():返回表示类型名称的字符串 fmt.Println(typeOf.Name()) // Student:它是一个Student结构体类型 } 复制代码
对于指针类型map、slice、chan这些引用类型,在Kind定义中有自己独属的种类。对于指针类型,Name返回的是内置的原生类型和在包中定义的类型,对于未定义的类型返回为空,Kind内置了Ptr用于表示。这也是为什么我们在大部分代码里会看到这么一段判断:
if typeof.Kind() == reflect.Ptr { typeof = typeof.Elem() } if typeof.Kind() != reflect.Struct { panic("bad type") } 复制代码
Elem()返回反射指针指向的元素类型,等效于对指针类型变量做了一个*操作。
经过判断后,在我们确保了typeOf可用的情况下,就可以通过reflect.Type的方法来获取所需要的信息。在上面的代码里使用了四个主要方法:
- NumField():获取字段的数量
- Field(index int):通过索引获取对应的字段类型
- Name():获取指定字段的名字
- Type():获取指定字段的类别
获取字段的方法有三种:
- 通过索引下标获取:Field(intdex int)/FieldByIndex(indexs int[]) 对于FieldByIndex方法,它是用来进行嵌套获取的,比如FieldByIndex(1, 2)等效于Field(1).Field(2)
- 通过字段名称获取:FieldByName(fieldName string)
- 通过正则匹配获取:FieldByNameFunc(match func(string) bool)
通过反射创建对象
获取了结构体的相关信息后,现在就是快乐的new对象时间,整体代码如下:
func ReadDataFromExcel(path string, mytype interface{}) []interface{} {
//......
for _, sheet := range file.Sheets{
for _, row := range sheet.Rows { // 每行读取
if idx == 0 { // 不读取Excel表的第一行
idx++
continue
}
newStruct := reflect.New(typeof)
for idx, cell := range row.Cells { // 每个单元格读取
text := strings.TrimSpace(cell.String())
newObject(text, newStruct, idx, idx)
}
data = append(data, newStruct)
}
}
//......
}
// 给反射对象赋值
func newObject(value string, newStruct reflect.Value, nameIdx, typeIdx int) {
switch fieldType[typeIdx] {
case "int", "int64", "int32":
num, err := strconv.Atoi(value)
if err != nil {
fmt.Println(err)
}
newStruct.Elem().FieldByName(fieldName[nameIdx]).SetInt(reflect.ValueOf(num).Int())
case "string":
newStruct.Elem().FieldByName(fieldName[nameIdx]).SetString(reflect.ValueOf(value).String())
case "bool":
newStruct.Elem().FieldByName(fieldName[nameIdx]).SetBool(reflect.ValueOf(value).Bool())
case "float32", "float64":
newStruct.Elem().FieldByName(fieldName[nameIdx]).SetFloat(reflect.ValueOf(value).Float())
default:
newStruct.Elem().FieldByName(fieldName[nameIdx]).SetString(reflect.ValueOf(value).String())
}
}
复制代码
首先我们像在java中使用Class.getDeclaredConstructor().newInstance()创建了一个对象一样来创建结构体:newStruct := reflect.New(typeof)。
每一行相当于一个结构体,而每行的每个单元格的值相当于结构体中对应字段的值。
所以在newObjec方法中,首先判断对应单元格的值是什么类型,然后将类型进行转换。由于Go是强类型语言,所以转换的步骤比较麻烦,但是看起来还是很整洁的。
看到这里,我们也可以发现其实没有必要单独创建结构体的【字段-->类型】映射,或者是切片。直接用上面提到过的Field方法就可以了。确实是这样,我这样做是在其它的地方需要使用,但是这不属于这篇文章的内容。
Value.Eelem()方法跟Type.Eelem()方法差不多,都是获取接口包含的值或者指针指向的对象。
对于反射对象的值的设置,我们可以通过SetXXX()来设置,我这里使用的模式差不多就是SetXXX(reflect.ValueOf(value).XXX())。
TypeOf是获取输入参数的值的类型,Value是获取输入参数中数据的值。
当执行reflect.ValueOf(value)后,会得到一个类型为relfect.Value的变量,可以通过它本身的Interface()方法获得接口变量的真实内容,然后可以通过类型判断进行转换,转换为原有真实类型。比如:
func main() { student := Student{ 1, "zs", "110", "001", } typeOf := reflect.ValueOf(student) fmt.Println(typeOf.Interface().(Student)) // 输出:{1 zs 110 001} } 复制代码
上面程序运行后的效果图为: