Posted in

Golang如何将Excel转为Struct?

作为一个程序员,我们免不了经常会跟Excel打交道,将列表导出为Excel,用Excel将订单信息导入到系统中,来回折腾。

Excel信息导入的功能实现也是大同小异:

  1. 解析Excel表格
  2. 拿到指定的sheet
  3. 读取sheet数据
  4. 便利每一行,每一列
  5. 将数据填充到结构体中
  6. 业务逻辑处理

这些步骤都大同小异,那么有没有方法,可以直接完成前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表头是

NumberName
xxxxx

那么这个 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
}

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注