作为一个程序员,我们免不了经常会跟Excel打交道,将列表导出为Excel,用Excel将订单信息导入到系统中,来回折腾。
Excel信息导入的功能实现也是大同小异:
- 解析Excel表格
- 拿到指定的sheet
- 读取sheet数据
- 便利每一行,每一列
- 将数据填充到结构体中
- 业务逻辑处理
这些步骤都大同小异,那么有没有方法,可以直接完成前5步呢?跟着我,咱们一步步来!
解析Excel文件
Golang的标准库功能非常有限,如果要完成一些复杂的功能,我们不得不借助一些第三方库,解析Excel文件就是一个非常复杂的功能,不过还好,我们有开源的第三方库 – Excelize(https://github.com/qax-os/excelize)
Excelize 是 Go 语言编写的用于操作 Office Excel 文档基础库,基于 ECMA-376,ISO/IEC 29500 国际标准。可以使用它来读取、写入由 Microsoft Excel™ 2007 及以上版本创建的电子表格文档。支持 XLAM / XLSM / XLSX / XLTM / XLTX 等多种文档格式,高度兼容带有样式、图片(表)、透视表、切片器等复杂组件的文档,并提供流式读写 API,用于处理包含大规模数据的工作簿。可应用于各类报表平台、云计算、边缘计算等系统。使用本类库要求使用的 Go 语言为 1.23.0 或更高版本。
操作步骤也是非常简单:
file, _, err := req.FormFile("file")
if err != nil {
fmt.Fprint(w, err.Error())
return
}
defer file.Close()
xlsx, err := excelize.OpenReader(file)
if err != nil {
fmt.Fprint(w, err.Error())
return
}
之后我们可以通过xlsx来拿到指定sheet的指定数据:
firstSheet := xlsx.GetSheetName(0)
rows, err := xlsx.GetRows(firstSheet)
if err != nil {
return err
}
但是这里问题就来了,rows不还是string slice吗?怎么转为结构体呢?我们接着看
row to struct
准确来说是 row to struct, 每一行数据对应一个struct,按照传统方法,我们需要这样做:
for _, row := range rows {
col1 := row[0]
col2 := row[1]
}
超级麻烦!而且,如果只是写一次那也就罢了,如果行中间加了一列,我们还需要修改对应的index,非常容易出错
所以,这种情况,我们可以用反射!这时候问题又来了:每次我需要的结构体都不一样,如何反射呢?怎么把列与结构体字段对应上呢?
非常好的问题!针对于结构体不一样,我们可以用1.8引入的泛型来实现,结构体与字段对于,我们可以在结构题上加上对应的tag标识,如下面的代码所示:
func Row2Stc[T any](headerMap map[string]int, row []string) T {
row = append(row, make([]string, len(headerMap)-len(row))...)
var stc = new(T)
val := reflect.ValueOf(stc)
if val.Kind() == reflect.Ptr {
val = val.Elem()
}
typ := val.Type()
for i := 0; i < val.NumField(); i++ {
field := typ.Field(i)
tag := field.Tag.Get("field")
fieldVal := row[headerMap[tag]]
val.Field(i).SetString(fieldVal)
}
return *stc
}
首先`row = append(row, make([]string, len(headerMap)-len(row))…)` 这一行代表的是将row的数据补齐,以防对应的单元格没有数据,row的长度不够,通过index取数是出现panic的现象。
headerMap是表头对应的index 的map,例如:一个sheet表头是
Number | Name |
xxx | xx |
那么这个 headerMap的值就应该是
headerMap := map[string]int{
"Number": 0,
"Name": 1,
}
tag := field.Tag.Get("field")
fieldVal := row[headerMap[tag]]
这一行意思是,获取到struct对应的名为 field的tag,然后拿到headerMap中对应的index,然后再从取出对应的值,然后通过 val.Field(i).SetString(fieldVal) 给结构体字段赋值。
所以我们需要这样一个结构体:
type Student struct {
Number string `json:"number" field:"Number"`
Name string `json:"name" field:"Name"`
}
拼凑到一起
package main
import (
"mime/multipart"
"reflect"
"github.com/xuri/excelize/v2"
)
var headerMap = map[string]int{
"Number": 0,
"Name": 1,
}
type Student struct {
Number string `json:"number" field:"Number"`
Name string `json:"name" field:"Name"`
}
func File2Stc[T any](file *multipart.File) ([]T, error) {
xlsx, err := excelize.OpenReader(*file)
if err != nil {
return nil, err
}
firstSheet := xlsx.GetSheetName(0)
rows, err := xlsx.GetRows(firstSheet)
if err != nil {
return nil, err
}
var res []T
for _, row := range rows {
item := Row2Stc[T](headerMap, row)
res = append(res, item)
}
return res, nil
}
func Row2Stc[T any](headerMap map[string]int, row []string) T {
row = append(row, make([]string, len(headerMap)-len(row))...)
var stc = new(T)
val := reflect.ValueOf(stc)
if val.Kind() == reflect.Ptr {
val = val.Elem()
}
typ := val.Type()
for i := 0; i < val.NumField(); i++ {
field := typ.Field(i)
tag := field.Tag.Get("field")
fieldVal := row[headerMap[tag]]
val.Field(i).SetString(fieldVal)
}
return *stc
}