您现在的位置是:网站首页> 编程资料编程资料
如何使用Golang创建与读取Excel文件_Golang_
2023-05-26
444人已围观
简介 如何使用Golang创建与读取Excel文件_Golang_
摘要
本文提出一种使用 Golang 进行 Excel 文件创建和读取的方案。首先对问题进行分析,引出方案的基本架构;然后分章节描述了 Excelize 基础库的基本用法,以及 Excel 数据在 Golang 中的表示和解析方式,并进一步提出了应对大规模数据写入场景的优化方法;最后,指出了一些可能遇到的问题和对策。
引言
飞书是业界领先的下一代企业协作与管理平台,集合了很多细分领域的优秀 ToB 产品。作者所在的部门,负责员工持股计划(Employee Stock Ownership Plan,ESOP)相关系统的研发,主要的后端开发语言为 Golang 。系统管理员,需要定期对公司 ESOP 的参与人信息,以及股权激励的授予、归属、取消等信息,以Excel表格的形式进行汇总,为相关决策提供参考和依据;必要时,也需要通过修改 Excel 数据表,上传系统,实现参与人、授予等信息的批量修改。
总而言之,随着 Golang 的广泛应用,如何使用 Golang 进行 Excel 数据表的读取和创建,是一个值得探讨的问题。本文将描述一套完整的 Excel 文件创建和读取的方案,方案力图做到灵活通用,和具体的业务数据类型无关,同时能够兼顾大规模数据导出的效率。最后,分享了一些研发过程中遇到的问题,希望能够避免读者再次踩坑。
正文
架构
在开始具体深入地描述我们的方案之前,不妨先思考一下,实现这样一个方案,需要解决的问题都有哪些?数据的处理大致应该是一个什么样的流程?下图是ESOP系统中,涉及到Excel文件创建和读取的部分业务场景。

文件对象
很自然地,我们需要引入一个 Excel 文件对象,此对象应该包含一个 Excel 工作簿的所有信息:有哪些工作表,每个工作表都有哪些列,每一行数据是什么,单元格和文本的格式是什么样的,某一列是否包含枚举值,等等。我们对文件的任何操作,无论是数据的增删改,还是格式样式的变更,亦或是文件的打开和保存,都应该将这个文件对象作为切入点。
数据的表示
一个 Excel 工作表,可以很自然地和 Golang 结构体联系起来,工作表的每一列对应结构体中的一个字段。然而,只定义一个基本的结构体还不够,至少还应该想办法保存每个字段对应的 Excel 列名、数据验证等等。
数据的解析
用 Golang 结构体表示了一个 Excel 工作表,自然还需要一种方法来解释结构体中记录的各种 Excel 数据信息,这样,程序代码才知道如何将结构体数据正确地写入文件对象,以及反过来,如何读取文件对象中的数据,来还原 Golang 结构体。数据表示和解析的整体思路,如下图所示。

实际架构
幸运的是,上面几个问题,我们都可以找到成熟且有力的工具来解决。方案基本的架构如下图所示。

文件对象的创建和各种操作,我们通过 Excelize 基础库来实现,后面会对该基础库进行简要介绍。
Excel 数据的表示,我们使用包含 tag 的 Golang 结构体实现,数据值以外的其他信息,用某种格式记录在 tag 中。
Excel 数据的解析,我们利用 Golang 的反射机制实现。通过反射,我们可以解析出结构体每个字段的值以及 tag 中保存的其他有用信息。
Excelize 基础库
Excelize 是一个使用 Golang 编写的,用于操作 Office Excel 文档的基础库,支持 Golang 1.15 及以上版本。下面对其基本用法进行介绍,涉及到的各 API 的具体用法,可查看文章末尾给出的Excelize Doc链接。
文件
文件对象是本文大部分 Excel 文件操作的入口。使用 NewFile 函数,可以创建一个空白的文件对象。如果需要用已有的 Excel 文件数据创建文件对象,可以使用 OpenReader 或者 OpenFile 函数。结束文件操作之后,通常需要将文件保存在本地,或者将文件输出为字节数组,返回给前端供用户下载,使用文件对象的 SaveAs 和 WriteToBuffer 方法,可以达到目的。
坐标
在使用更多功能之前,必须搞清楚如何定位一个单元格或一个区域。
Excel 中使用形如“A1”这样的坐标来定位单元格。相应的,在 Excelize 基础库中,可以通过 CoordinatesToCellName 函数,将行号和列号这个二元组,转换为一个形如“A1”的坐标。一些操作,需要通过两个坐标来确定生效区域,此时,两个相同的坐标表示对一个单元格生效,两个不同的坐标表示对一个矩形区域生效,分别指向区域左上角和右下角的单元格。
样式
样式包含字体、文字大小、粗细、位置、颜色等属性。Excelize 中,样式可以通过 NewStyle 方法创建,返回一个整数索引,唯一标识这个样式。通过文件对象的 SetCellStyle 方法,指定坐标和样式索引,可以为一个区域设置统一的样式。
单元格操作
单元格的常用操作有设置高度和宽度、合并单元格、设置单元格数据等。
我们针对一行设置高度,针对一列设置宽度,分别使用文件对象中的 SetRowHeight 和 SetColWidth 方法进行。
在“坐标”部分,我们讲解了如何确定一个区域,合并单元格的操作,就是一个例子,我们可以使用文件对象中的 MergeCell 方法来完成。
一般情况下,数据的写入操作,在单元格层面进行。使用文件对象中的 SetCellValue 方法,指定一个坐标,可以将 Golang 中常用的数据类型(包括无符号整数、有符号整数、浮点数、字节切片、字符串、时间、布尔类型等等)的值写入对应的单元格。
数据验证
数据验证功能,可以为某一列数据定义枚举值,用户可以使用下拉列表,为该列中某一行选择要填入的值。
使用 NewDataValidation 函数,可以创建一个数据验证对象,不妨命名为 vd 。为了指定 vd 的生效范围,需要为 vd 设置一个 Sqref 属性,格式形如“A1:A10”,表示此 vd 对第 1 列中第 1 行到第 10 行的数据生效。然后,使用 vd 的 SetDropList 方法,设置下拉列表的内容。最后,使用文件对象中的 AddDataValidation 方法,将此 vd 写入文件。
数据的表示和解析
表示
根据“架构”部分的设想,我们可以定义一个带有 tag 的结构体,来表示一个 Excel 工作表。
Golang 结构体的 tag ,是以键值对的形式表示的。为了和其他用途的 tag 进行区分,我们将本方案的 tag 信息,用一个名为 ex 的键来表示,而 ex 的值,仍然沿用键值对的形式,如下列代码所示:
type PeopleInfo struct { PeopleNo string `ex:"head:工号;type:string;required;color:#0070C0"` PeopleName string `ex:"head:姓名;type:string;required"` BirthDate string `ex:"head:出生日期;type:date;omitempty"` EmploymentStatus string `ex:"head:在职状态;type:string;select:在职,离职"` } 我们可以为ex设计下列属性:
- head,指定了此结构体字段对应的 Excel 列名。
- type,表示在使用反射进行数据解析时,会将此结构体字段的值作为指定的类型处理。
- select,表示此字段所在的列,包含一个下拉列表,列表中的枚举值由 select 后面的值指定。
- required,表示此字段必须包含非零值,否则在写入 Excel 时会报错。
- omitempty,表示此字段如果是零值,则对应的单元格留空。
- color,指定了列名所在单元格的颜色,通过这个字段,可以为不同的列名设置不同的底色,赋予一些含义,例如,可以将必填的列和选填的列,设置不同的底色。可以通过 Excel 的 RGB 颜色设置窗口,查看不同颜色对应的色号,作为 color 属性的值。
此外,我们还要定义一个结构体,保存 ex 的解析结果,结构体不妨命名为 Setting :
type Setting struct { Head string Type string Select []string Required bool OmitEmpty bool Color string } 解析
使用 Golang 的反射机制,对类似于 PeopleInfo 这样的结构体,我们可以抽取每个字段的ex值,进行字符串处理后,组装成Setting对象。示例代码如下:
import reflect // 解析第idx个字段的ex func ParseEx(idx int, data interface{}) *Setting { tp := reflect.ValueOf(data).Type().Elem().Elem() // 获得结构体的反射Type field := tp.Field(idx) exStr := field.Tag.Get("ex") // 获得tag中ex的值 setting := &Setting{} // 下面可通过对exStr字符串进行切分,来组装Setting对象,较为简单,省略 ... return setting } func main() { ParseEx(0, []*PeopleInfo{{}}) } 由于反射机制较为抽象,这里不再赘述,对反射不熟悉的读者,可以查看文章末尾给出的 Golang reflect 链接。
组装了 Setting 之后,我们可以继续通过反射,来获取结构体中各字段的值,然后使用前面介绍过的一些 API ,将这些信息写入 Excel 文件。
下面给出创建 Excel 文件的示例代码,代码对 omitempty 和 type 属性进行了处理,并将部分数据写入文件对象。其他 ex 属性的处理,因篇幅有限,不再演示,读者有兴趣可以自己尝试实现。
import reflect import "github.com/xuri/excelize/v2" // 写入第1行数据的第idx个字段 func WriteFirstRow(ef *excelize.File, idx int, data interface{}) error { firstRow := reflect.ValueOf(data).Index(0).Elem() // 第1个数据的反射Value v := firstRow.Field(idx) // 第idx个字段的反射Value setting := ParseEx(idx, data) // 第idx个字段解析出来的ex信息 // 处理omitempty if setting.OmitEmpty && v.IsZero() { return nil } var val interface{} // 处理type switch setting.Type { case "string": val = v.String() case ... } // Excel列号从1开始,所以列号是idx+1;行号从2开始,因为第1行要显示列名 axis, err := excelize.CoordinatesToCellName(idx+1, 2) if err != nil { return err } // 将数据写入默认工作表Sheet1中axis坐标处的单元格 return ef.SetCellValue("Sheet1", axis, val) } func main() { ef := excelize.NewFile() WriteFirstRow(ef, 0, []*PeopleInfo{{PeopleNo: "test"}}) ef.SaveAs("people_info.xlsx") } 上面给出的是创建 Excel 文件的示例。读取 Excel 文件的过程是类似的,首先从二进制数据创建出文件对象,然后根据文件对象中的每一列数据,生成对应的结构体对象。示例代码如下。
import reflect import "github.com/xuri/excelize/v2" // 读取第1行数据的第idx列,假定idx从0开始,只有一个默认工作表Sheet1,数据从第2行开始 func ReadFirstRow(ef *excelize.File, idx int, holder interface{}) error { rows, err := ef.GetRows("Sheet1") // 所有行 if err != nil { return err } row := rows[1] tp := reflect.TypeOf(holder).Elem().Elem().Elem() // 结构体的类型 val := reflect.New(tp) // 创建一个新的结构体对象 field := val.Elem().Field(idx) // 第idx个字段的反射Value cellValue := row[idx] // 第idx个字段对应的Excel数据 field.SetString(cellValue) // 将Excel数据保存到结构体对象的对应字段中 listV := reflect.ValueOf(holder) listV.Elem().Set(reflect.Append(listV.Elem(), val)) // 将结构体对象添加到holder中 return nil } func main() { ef, _ := excelize.OpenFile("people_info.xlsx") holder := make([]*PeopleInfo, 0, 10) ReadFirstRow(ef, 0, &holder) } 本节描述了如何使用 Golang 来表示和解析 Excel 数据,以及在此基础上如何创建和读取 Excel 文件。示例代码中对 Excel 文件的写入和读取操作函数,使用 interface 类型的参数作为数据提供方或接收方,和具体的业务数据类型无关,因此该方案具备通用性。
大规模数据的写入
之前演示的 Excel 文件写入方式,是在单元格层面进行的,在大规模数据写入的场景下,耗时长,体验差。Excelize 提供了一套流式写入 API,以行为单位写入 Excel 数据,能够显著提高大规模数据的写入效率。
使用流式 API 写入 Excel 数据,首先需要使用文件对象的 NewStreamWriter 方法,创建一个流式写入器。写入一行数据时,需要构造一个切片,表示这一行数据,切片中每个元素表示一个单元格信息,包含单元格的值和样式。单元格元素,可以使用 Excelize 中提供的 Cell 数据类型来表示。之后,就可以通过流式写入器的 SetRow 方法,将行数据写入 Excel 文件。行的高度,可以在写入时指定。示例代码如下:
import reflect import "github.com/xuri/excelize/v2" // 写入第1行数据 func StreamWriteFirstRow(sw *excelize.StreamWriter, data interface{}) error { firstRow := reflect.ValueOf(data).Index(0).Elem() // 第1个数据的反射Value v := firstRow.Field(0) // 首个字段的反射Value style := &excelize.Style{ Alignment: &excelize.Alignment{ Horizontal: "left", Vertical
点击排行
本栏推荐
