反射是所有面向对象语言的一个重点,它为开发者提供了灵活的操作可能。利用反射可以获取不同对象/结构体的信息,制定不同的策略,实现复杂的操作。

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方法里,我们需要进行三个步骤:

  1. 获取结构体信息
  2. 获取Excel表信息
  3. 通过反射创建对象

首先把完整的获取结构体信息的代码贴出来,然后对此进行分析:

// 字段类型切片
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():获取指定字段的类别

获取字段的方法有三种:

  1. 通过索引下标获取:Field(intdex int)/FieldByIndex(indexs int[]) 对于FieldByIndex方法,它是用来进行嵌套获取的,比如FieldByIndex(1, 2)等效于Field(1).Field(2)
  2. 通过字段名称获取:FieldByName(fieldName string)
  3. 通过正则匹配获取: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} } 复制代码

上面程序运行后的效果图为: