背景

起因于最近的一项工作:我们会定义一些关键指标来衡量当前系统的健康状态,然后配置对应的报警规则来进行监控报警。但是当前的报警规则会产生大量的误报,需要进行优化。我所负责的是将一些和用户行为指标相关的报警规则拆封从日间和夜间两套规则(因为在夜间用户的使用量减少,报警的阈值是可以调高的)。

这实际上就是一个体力活儿,把原来的报警规则再复制一份,然后改一下阈值。但我算了一个,原来大概有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.Typereflect.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) {} 可以实现更新变量

三大法则

  1. interface{}变量反射出反射对象

    reflect.TypeOf的入参类型是interface{},所以当我们调用时,会把原来的强类型对象变成interface{}类型的拷贝(Go 中函数传参是值传递)传到函数内部。所以说,使用reflect.TypeOfreflect.ValueOf能够获取 Go 语言中的变量对应的反射对象。一旦获取了反射对象,我们就能得到跟当前类型相关数据和操作,并可以使用这些运行时获取的结构执行方法。

  2. 从反射对象对象可以获取interface{} 变量

    reflect.Value.Interfac方法可以帮助我们将一个反射对象(reflect.Value)变回interface{}对象。也就是说,我们通reflec包可以实现反射对象interface{}对象之间的自由切换:

  3. 要修改反射对象,其值必须可设置

    这一点很重要,如果我们想更新一个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
    }

有用的方法

  1. 获取到结构体标签
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
  1. 给结构体中字段赋值

正常地结构体字段赋值,我们都是通过字面量的方式去做:

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了。

  1. 构建结构体标签与字段的映射

我们就把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]
  1. 读取 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]
  1. 将 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的更多相关文章

  1. 利用反射实现通用的excel导入导出

    如果一个项目中存在多种信息的导入导出,为了简化代码,就需要用反射实现通用的excel导入导出 实例代码如下: 1.创建一个 Book类,并编写set和get方法 package com.bean; p ...

  2. 反射+自定义注解---实现Excel数据列属性和JavaBean属性的自动映射

    简单粗暴,直奔主题.   需求:通过自定义注解和反射技术,将Excel文件中的数据自动映射到pojo类中,最终返回一个List<pojo>集合? 今天我只是通过一位使用者的身份来给各位分享 ...

  3. 利用反射将Datatable、SqlDataReader转换成List模型

    1. DataTable转IList public class DataTableToList<T>whereT :new() { ///<summary> ///利用反射将D ...

  4. .net 将excel转成html文件

    最近在做一个打印预览功能,但是开始没有头绪后来用excel做了一个模板,然后根据excel模板来生成新的excel并将其存储为html,可以通过http请求在浏览器中读取,并且打印,其他的不多说.方法 ...

  5. 利用 js-xlsx 实现 Excel 文件导入并解析Excel数据成json格式的数据并且获取其中某列数据

    演示效果参考如下:XML转JSON 另一个搭配SQL实现:http://sheetjs.com/sexql/index.html 详细介绍: 1.首先需要导入js <script src=&qu ...

  6. Epplus下的一个将Excel转换成List的范型帮助类

    因为前一段时间公司做项目的时候,用到了Excel导入和导出,然后自己找了个插件Epplus进行操作,自己将当时的一些代码抽离出来写了一个帮助类. 因为帮助类是在Epplus基础之上写的,项目需要引用E ...

  7. 利用泛型和反射,管理配置文件,把Model转换成数据行,并把数据行转换成Model

    利用泛型和反射,管理配置文件,把Model转换成数据行,并把数据行转换成Model   使用场景:网站配置项目,为了便于管理,网站有几个Model类来管理配置文件, 比如ConfigWebsiteMo ...

  8. NPOI操作EXCEL(四)——反射机制批量导出excel文件

    前面我们已经实现了反射机制进行excel表格数据的解析,既然有上传就得有下载,我们再来写一个通用的导出方法,利用反射机制实现对系统所有数据列表的筛选结果导出excel功能. 我们来构想一下这样一个画面 ...

  9. 利用java反射机制实现读取excel表格中的数据

    如果直接把excel表格中的数据导入数据库,首先应该将excel中的数据读取出来. 为了实现代码重用,所以使用了Object,而最终的结果是要获取一个list如List<User>.Lis ...

随机推荐

  1. MOSFET, MOS管, 开关管笔记

    MOSFET, MOS管, 开关管 MOSFET, Metal-Oxide-Semiconductor Field-Effect Transistor, 金属氧化物半导体场效晶体管 常见封装 电路符号 ...

  2. js通用对象数组冒泡排序

    数组对象通用 function sort(data, sortFiled, orderby) { var result = data, temp; for (var i = 0; i < res ...

  3. 一起看 I/O | Flutter 休闲游戏工具包发布

    作者 / Zoey Fan, Product Manager for Flutter, Google 对于大多数开发者来说,Flutter 是一个应用框架.但利用 Flutter 提供的硬件加速图形支 ...

  4. 设计并实现加法器类 Adder

    学习内容:设计并实现加法器类 Adder 代码示例: package 实验三; import java.util.Scanner; public class Adder { private int n ...

  5. Vue路由实现之通过URL中的hash(#号)来实现不同页面之间的切换(图表展示、案例分析、附源码详解)

    前言 本篇随笔主要写了Vue框架中路由的基本概念.路由对象属性.vue-router插件的基本使用效果展示.案例分析.原理图解.附源码地址获取. 作为自己对Vue路由进行页面跳转效果知识的总结与笔记. ...

  6. 【单片机】CH32V103C8T6 ——窗口看门狗

    本章教程通过串口调试助手打印显示程序运行状态,具体现象如下: 若计数器值在上窗口值和下窗口值0X40之间的时候,进行喂狗操作,计数器重新计数,程序正常运行,串口打印显示:The program run ...

  7. Kafka 消费者解析

    一.消费者相关概念 1.1 消费组&消费者 消费者: 消费者从订阅的主题消费消息,消费消息的偏移量保存在Kafka的名字是__consumer_offsets的主题中 消费者还可以将⾃⼰的偏移 ...

  8. 什么是Netty编解码,Netty编解码器有哪些?Protostuff怎么使用?

    哈喽!大家好,我是小奇,一位热爱分享的程序员 小奇打算以轻松幽默的对话方式来分享一些技术,如果你觉得通过小奇的文章学到了东西,那就给小奇一个赞吧 文章持续更新 一.前言 书接上回,昨天下雨没怎么上街上 ...

  9. 记一次 JDK SPI 配置不生效的问题 → 这么简单都不会,还是回家养猪吧

    开心一刻 今天去幼儿园接小侄女,路上聊起了天 小侄女:小叔,今天我吃东西被老师发现了 我:老师说了什么 小侄女:她说拿出来,跟小朋友一起分享 我:那你拿出来了吗 小侄女一脸可怜的看向我,说道:没有,我 ...

  10. 【原创】史上最简单易懂的kali修改主机名方法

    前言 主机名:在一个局域网中,每台机器都有IP地址,但是IP地址不便于人们记忆.于是采用主机名,即利于主机之间的区分,又方便人们的记忆主机.--和域名差不多. 一,查看当前的主机名 ┌──(root㉿ ...