小试牛刀:Go 反射帮我把 Excel 转成 Struct

背景
起因于最近的一项工作:我们会定义一些关键指标来衡量当前系统的健康状态,然后配置对应的报警规则来进行监控报警。但是当前的报警规则会产生大量的误报,需要进行优化。我所负责的是将一些和用户行为指标相关的报警规则拆封从日间和夜间两套规则(因为在夜间用户的使用量减少,报警的阈值是可以调高的)。
这实际上就是一个体力活儿,把原来的报警规则再复制一份,然后改一下阈值。但我算了一个,原来大概有100多个报警规则,这还是一个不小的力气活儿啊!万幸的是,我们的报警平台是支持通过 json 文件的方式导入规则的,我可以使用PythonGo写一个简单的程序(最开始是用 Python 写的,但想提高一下 Go 的熟练度,又用 Go 写了一版):使用代码生成出可被报警平台解析的 json 文件。
为了保持规则的可维护性,我决定把规则的核心参数(比如:指标参数、时间、阈值等)放在在线 Excel 进行保存,编辑完后,下载到本地,通过一个简单的程序生成 json 文件,导入到报警平台。
解析 Excel 文件是通过github.com/xuri/excelize/v2这个库在做的,但它只能把每行解析成string类型的切片,还需要我去一个一个转成我定义的结构体对应字段的类型,然后再去赋值。我想到:如果能像json.Unmarshal一样,可以自动进行类型转换,并且复值给结构体中的对应字段,那就好了!
json.Unmarshal 是怎么做到的?
type Stu struct {
   Name string `json:"name"`
   Age int32   `json:"age"`
}
func main() {
   data := `{"name": "Zioyi", "age": 1}`
   s1 := Stu{}
   _ = json.Unmarshal([]byte(data), &s1)
   fmt.Printf("%+v\n", s1)
}
$ > go run main.go
{Name:Zioyi Age:1}
为什么它可以把
Zioyi赋值给Name字段,把1赋值给age字段?
可以注意到,在定义结构体 Stu 时,在每个字段的类型后面,有一段被反引号包含的内容:json:"name"、 json:"Age"。这实际上 Go 的一个特性:结构体标签,通过它将被结构体字段和 json 数据中的 key 进行了绑定,使得再调用 json.Unmarshal 时,可以把 json 数据中的 value 准确的赋值给结构体字段。
那如何在运行时,取到结构体标签的呢?实际上是接助了 Go 的反射能力。
反射
众所周知,Go 一门强类型语言,这在保证程序运行安全的同时,也为程序编码增加了也许不便。而反射机制,便提供了一种能力:在编译时不知道类型的情况下,可更新变量、在运行时查看值、调用方法以及直接对它们的布局进行操作。
Go 将反射negligible都封装在了reflect包,包内有两对非常重要的函数和类型,两个函数分别是:
- reflect.TypeOf函数接收任意的 interface{} 参数,并且把接口的动态类型以- refelct.Type的形式返回
- reflect.VauleOf函数接收任意的 interface{} 参数,并且把接口的动态值以- refelct.Type的形式返回
两个类型是reflect.Type和reflect.Value,它们与函数是一一对应的关系:
reflect.Type是一个接口,通过调用reflect.TypeOf函数可以后去任意变量的类型。这个接口绑定了很多有用的方法:MethodByName可以获取当前类型对应方法的引用、Implements 可以判断当前类型是否实现了某个接口、Field可以根据下标取到结构体字段的应用等。
type Type interface {
    Align() int
    FieldAlign() int
    Method(int) Method
    MethodByName(string) (Method, bool)
    NumMethod() int
    Field(i int) StructField
    FieldByIndex(index []int) StructField
    ...
    Implements(u Type) bool
    ...
}
reflect.Value是一个结构体
type Value struct {
   typ *rtype
   ptr unsafe.Pointer
   flag
}
但它没有可导出的字段,需要通过方法来访问,有两个比较重要的方法:
- func (v Value) Elem() Value {}返回制作指向的具体数据
- func (v Value) SetT(x T) {}可以实现更新变量
三大法则
- 从 - interface{}变量反射出反射对象- reflect.TypeOf的入参类型是- interface{},所以当我们调用时,会把原来的强类型对象变成- interface{}类型的拷贝(Go 中函数传参是值传递)传到函数内部。所以说,使用- reflect.TypeOf和- reflect.ValueOf能够获取 Go 语言中的变量对应的反射对象。一旦获取了反射对象,我们就能得到跟当前类型相关数据和操作,并可以使用这些运行时获取的结构执行方法。
- 从反射对象对象可以获取 - interface{}变量- reflect.Value.Interfac方法可以帮助我们将一个反射对象(- reflect.Value)变回- interface{}对象。也就是说,我们通- reflec包可以实现- 反射对象与- interface{}对象之间的自由切换:
 
- 要修改反射对象,其值必须可设置 - 这一点很重要,如果我们想更新一个 - reflect.Value,那必须是可以被更新的。含义是,如果当我们调用- reflect.ValueOf直接传入了变量,由于 Go 语言的函数调用都是传值的,所以我们得到的反射对象跟最开始的变量没有任何关系,那么直接修改反射对象无法改变原始变量。所以我们应该传入的是原来变量的地址,然后通过- refect.Value.Elem取到指针指向的变量再去修改。- func badCase() {
 i := 1
 v := reflect.ValueOf(i)
 v.SetInt(10) // panic: reflect: reflect.flag.mustBeAssignable using unaddressable value
 fmt.Println(i)
 } func goodCase() {
 i := 1
 v := reflect.ValueOf(&i)
 v.Elem().SetInt(10)
 fmt.Println(i) // 10
 }
 
有用的方法
- 获取到结构体标签
type Stu struct {
   Name string `json:"name"`
   Age int32   `json:"age"`
}
上面提到过,reflect.Type接口中有一个方法Field,他可以通过下标返回结构体中的第X个字段(类型为FieldStruct)
func main () {
    u := reflect.TypeOf(Stu{})
    f := u.Field(0)
    fmt.Printf("%+v\n", f)
    v, ok := f.Tag.Lookup("json")
    fmt.Printf("tag value is %s, ok is %t\n", v, ok)
}
$ > go run main.go
{Name:Name PkgPath: Type:string Tag:json:"name" Offset:0 Index:[0] Anonymous:false}
tag value is name, ok is true
根据打印出的内容可以看到,StructField结构体中的Tag字段就保存了标签信息,并且非常人性化地提供了Lookup方法找到我们想要 tag 值。
而且,reflect.Type接口中有一个方法NumField可以获取到结构体的字段总数,这样我们就可以结合起来去遍历了:
func main () {
   u := reflect.TypeOf(Stu{})
   num := u.NumField()
   fmt.Printf("Struct Str total field count:%+v\n", num)
   for i := 0; i < num; i++ {
      f := u.Field(i)
      v, _ := f.Tag.Lookup("json")
      fmt.Printf("field %s, tag value is %s\n", f.Name, v)
   }
}
$ > go run main.go
Struct Str total field count:2
field Name, tag value is name
field Age, tag value is age
- 给结构体中字段赋值
正常地结构体字段赋值,我们都是通过字面量的方式去做:
s := Stu{}
s.Name = "Zioyi"
而reflect.Value.Set方法提供给了我们去更新反射对象的能力,我们可以这样做:
func main () {
   s1 := Stu{}
   u := reflect.ValueOf(&s1)           // 获取反射对象
   fv := u.Elem().FieldByName("Name")  // 通过字段名获取字段的反射对象
   fv.SetString("Zioyi")               // 等价于 s1.Name = "Zioyi"
   fv = u.Elem().Field(1)              // 通过字段下标获取字段的反射对象
   fv.SetInt(1)                        // 等价于 s1.Age = 1
   fmt.Printf("%+v\n", s1)
}
$ > go run main.go
{Name:Zioyi Age:1}
Excel to Struct
通过上面的介绍,我们已经掌握了reflect的基本用法,我们已经可以是想一个xslx版的Unmarshal了。
- 构建结构体标签与字段的映射
我们就把xlsx作为标签的 key
type Stu struct {
   Name string `xlsx:"name"`
   Age int32   `xlsx:"age"`
}
然后通过上面提到的Field方法来提取结构体Stu的字段和标签
func initTag2FieldIdx(v interface{}, tagKey string) map[string]int {
   u := reflect.TypeOf(v)
   numField := u.NumField()
   tag2fieldIndex := map[string]int{}
   for i := 0; i < numField; i++ {
      f := u.Field(i)
      tagValue, ok := f.Tag.Lookup(tagKey)
      if ok {
         tag2fieldIndex[tagValue] = i
      } else {
         continue
      }
   }
   return tag2fieldIndex
}
func main () {
   initTag2FieldIdx := initTag2FieldIdx(Stu{}, "xlsx")
   fmt.Printf("%+v\n", initTag2FieldIdx)
}
$ > go run main.go
map[age:1 name:0]
- 读取 xslx 文件内容
 
func getRows() [][]string {
   file, err := excelize.OpenFile("stu.xlsx")
   if err != nil {
      panic(err)
   }
   defer file.Close()
   rows, err := file.GetRows("Stu", excelize.Options{})
   if err != nil {
      panic(err)
   }
   return rows
}
func main () {
   rows := getRows()
   for _, row := range rows {
      fmt.Printf("%+v\n", row)
   }
}
$ > go run main.go
[name age]
[Zioyi 1]
[Bob 12]
- 将 xlsx 文件内容转成 Stu 结构体
我们默认 xlxs 的第一行描述了每列对应 Stu 的字段,我们可以通过上面的提到的Vaule.Set方法进行赋值
func rowsToStus (rows [][] string, tag2fieldIndex map[string]int) []*Stu {
   var data []*Stu
   // 默认第一行对应tag
   head := rows[0]
   for _, row := range rows[1:] {
      stu := &Stu{}
      rv := reflect.ValueOf(stu).Elem()
      for i := 0; i < len(row); i++ {
         colCell := row[i]
         // 通过 tag 取到结构体字段下标
         fieldIndex, ok := tag2fieldIndex[head[i]]
         if !ok {
            continue
         }
         colCell = strings.Trim(colCell, " ")
         // 通过字段下标找到字段放射对象
         v := rv.Field(fieldIndex)
         // 根据字段的类型,选择适合的赋值方法
         switch v.Kind() {
         case reflect.String:
            value := colCell
            v.SetString(value)
         case reflect.Int64, reflect.Int32:
            value, err := strconv.Atoi(colCell)
            if err != nil {
               panic(err)
            }
            v.SetInt(int64(value))
         case reflect.Float64:
            value, err := strconv.ParseFloat(colCell, 64)
            if err != nil {
               panic(err)
            }
            v.SetFloat(value)
         }
      }
      data = append(data, stu)
   }
   return data
}
func main() {
   initTag2FieldIdx := initTag2FieldIdx(Stu{}, "xlsx")
   rows := getRows()
   stus := rowsToStus(rows, initTag2FieldIdx)
   for _, s := range stus {
      fmt.Printf("%+v\n",s)
   }
}
$ > go run main.go
&{Name:Zioyi Age:1}
&{Name:Bob Age:12}
到这里,我们就完成了xslx版本的Unmarshal操作。
小试牛刀:Go 反射帮我把 Excel 转成 Struct的更多相关文章
- 利用反射实现通用的excel导入导出
		如果一个项目中存在多种信息的导入导出,为了简化代码,就需要用反射实现通用的excel导入导出 实例代码如下: 1.创建一个 Book类,并编写set和get方法 package com.bean; p ... 
- 反射+自定义注解---实现Excel数据列属性和JavaBean属性的自动映射
		简单粗暴,直奔主题. 需求:通过自定义注解和反射技术,将Excel文件中的数据自动映射到pojo类中,最终返回一个List<pojo>集合? 今天我只是通过一位使用者的身份来给各位分享 ... 
- 利用反射将Datatable、SqlDataReader转换成List模型
		1. DataTable转IList public class DataTableToList<T>whereT :new() { ///<summary> ///利用反射将D ... 
- .net 将excel转成html文件
		最近在做一个打印预览功能,但是开始没有头绪后来用excel做了一个模板,然后根据excel模板来生成新的excel并将其存储为html,可以通过http请求在浏览器中读取,并且打印,其他的不多说.方法 ... 
- 利用 js-xlsx 实现 Excel 文件导入并解析Excel数据成json格式的数据并且获取其中某列数据
		演示效果参考如下:XML转JSON 另一个搭配SQL实现:http://sheetjs.com/sexql/index.html 详细介绍: 1.首先需要导入js <script src=&qu ... 
- Epplus下的一个将Excel转换成List的范型帮助类
		因为前一段时间公司做项目的时候,用到了Excel导入和导出,然后自己找了个插件Epplus进行操作,自己将当时的一些代码抽离出来写了一个帮助类. 因为帮助类是在Epplus基础之上写的,项目需要引用E ... 
- 利用泛型和反射,管理配置文件,把Model转换成数据行,并把数据行转换成Model
		利用泛型和反射,管理配置文件,把Model转换成数据行,并把数据行转换成Model 使用场景:网站配置项目,为了便于管理,网站有几个Model类来管理配置文件, 比如ConfigWebsiteMo ... 
- NPOI操作EXCEL(四)——反射机制批量导出excel文件
		前面我们已经实现了反射机制进行excel表格数据的解析,既然有上传就得有下载,我们再来写一个通用的导出方法,利用反射机制实现对系统所有数据列表的筛选结果导出excel功能. 我们来构想一下这样一个画面 ... 
- 利用java反射机制实现读取excel表格中的数据
		如果直接把excel表格中的数据导入数据库,首先应该将excel中的数据读取出来. 为了实现代码重用,所以使用了Object,而最终的结果是要获取一个list如List<User>.Lis ... 
随机推荐
- 现有教学数据库JX_DB,作业
			现有教学数据库JX_DB,数据库有以下三个基本表: 学生表student,它由学号sno.姓名sname.性别sex.出生日期Bdate.所在系dept五个属性构成.其中,学号不能为空,值是唯一的: ... 
- mysql5.7介绍和安装
			环境准备: 1.关闭防火墙和selinux systemctl stop firewalldsystemctl stop SElinux 2. 如果安装过mariadb需要停止且卸载服务 system ... 
- windows获取高精度时间戳 精度100ns
			#include <stdio.h> #include <Windows.h> int main(void){ LARGE_INTEGER ticks,Frequency; Q ... 
- TypeScript 学习的随笔
			TypeScript 是 JavaScript 的一个超集,支持 ECMAScript 6 标准 安装TypeScript npm install -g typescript 编译 tsc app.t ... 
- 编程语言与python与pycharm的下载
			目录 编程语言的发展史 编程语言的分类 python解释器 python解释器的下载与安装 环境变量 执行python程序方式 pycharm编辑器 编程语言的发展史 机器语言是最开始的编程语言,之后 ... 
- [USACO16JAN]Angry Cows G 解题报告
			一图流 参考代码: #include<bits/stdc++.h> #define ll long long #define db double #define filein(a) fre ... 
- JavaSE_多线程入门 线程安全 死锁 状态 通讯 线程池
			1 多线程入门 1.1 多线程相关的概念 并发与并行 并行:在同一时刻,有多个任务在多个CPU上同时执行. 并发:在同一时刻,有多个任务在单个CPU上交替执行. 进程与线程 进程:就是操作系统中正在运 ... 
- point pair feature在2D图像匹配中的应用
			point pair feature在2D图像匹配中的应用 point pair feature(ppf) @article{BertramDrost2010ModelGM, title={Model ... 
- 2020.12.12【NOIP提高B组】模拟 总结
			第一次来 B 组做,虚的很 T1: 容斥原理 比赛时也打了个大致,但挂了,只有 50 分. 赛后重构了一下代码,AC \(UPDATE:2020/12/13\ \ \ 14:10\) 思路: 像前缀和 ... 
- 聊聊 C++ 和 C# 中的 lambda 玩法
			这几天在看 C++ 的 lambda 表达式,挺有意思,这个标准是在 C11标准 加进去的,也就是 2011 年,相比 C# 2007 还晚了个 4 年, Lambda 这东西非常好用,会上瘾,今天我 ... 
