Go-excelize库实现 excel web 端导出的最佳实践

通常在后端开发中遇到Excel文件导出是一件常见的需求,在数据量小的情况下,这个功能的开发是很简单的,直接读取数据到内存中一次性写入excel中导出即可,但是通常导出的数据量会比较大,可能会有几十万生成上百万条,这时你就要考虑很多场景,不然你可能会遇到以下问题:

  • 接口超时;
  • 一次性加载数据到内存中,导致内存溢出;
  • EXCEL文件过大,无法打开等问题;

那要如何规避这些问题呢,我们接着往下聊。


1. 超时

引起超时的原因有多种,比如说数据查询慢、写入excel慢、数据库连接慢、资源占用高服务响应慢等等,我们主要关注下查询和写入两个场景。

1.1 数据查询慢

通常对于大批量的数据导出,我们会使用分页的方式,比如每次查询1000条数据,然后写入到excel中。这里就会遇到一个分页查询的问题,没有正确的使用索引和覆盖索引(避免回表)都会导致查询效率降低,进而影响到导出的速度。

使用覆盖索引的示例:

sql 复制代码
SELECT * FROM table
JOIN (
  SELECT id FROM table
  ORDER BY create_time 
  LIMIT {offset}, {size}
) AS tmp USING(id);

1.2 写入excel慢

首先要避免单行写入,尽量用流式写入,对于大数据导出的场景要分sheet页或者多文件处理,我这里比较推荐多文件,然后压缩写入zip包中。


2. 内存溢出

通常情况下内存溢出,一般是由于一次性将数据加载到内存,或多次加载到内存后,再进行写入excel操作导致的。那要避免这种问题就要将每次查询到的内容先流式写入的excel中,然后进行下一次查询,直到全部查询完。


3. EXCEL文件过大,无法打开

首先减少一个excel文件中的数据量,对于大批量数据的导出,可视情况将数据拆分到多个sheet页或多个excel文件中;还可以考虑其它数据格式的替代,比如csv文件(通常文件大小只有excel文件的1/10)。


4. excelize库实现多excel文件写入zip导出

以下是一个使用 excelize 结合 zip 写入的通用代码模板,满足以下要求:

  • 分excel文件,写入到zip压缩包中导出;
  • 使用 gin 框架支持 Web 同步下载;
  • 每个 Excel 文件使用流式写入;
  • 每写完一个 Excel 就立即写入到 ZIP 中,避免占用过多内存;
  • 表头通过反射自动生成;
  • 支持任意结构体类型的导出,便于复用与扩展。
go 复制代码
package main

import (
	"archive/zip"
	"bytes"
	"fmt"
	"net/http"
	"reflect"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/xuri/excelize/v2"
)

type ComponentDetail struct {
	ID      string `excel:"ID"`
	Name    string `excel:"名称"`
	Version string `excel:"版本"`
	Time    string `excel:"创建时间"`
}

func main() {
	r := gin.Default()
	r.GET("/export", exportHandler)
	r.Run(":8080")
}

func exportHandler(c *gin.Context) {
	c.Header("Content-Type", "application/zip")
	c.Header("Content-Disposition", `attachment; filename="export.zip"`)

	// 创建 zip writer
	zipWriter := zip.NewWriter(c.Writer)
	defer zipWriter.Close()

	// 模拟多个组件导出
	for i := 0; i < 3; i++ {
		componentID := fmt.Sprintf("component_%d", i+1)

		// 模拟数据
		var data []ComponentDetail
		for j := 0; j < 10; j++ {
			data = append(data, ComponentDetail{
				ID:      fmt.Sprintf("%s_item_%d", componentID, j+1),
				Name:    fmt.Sprintf("组件名-%d", j+1),
				Version: "v1.0",
				Time:    time.Now().Format("2006-01-02 15:04:05"),
			})
		}

		// 每写完一个 Excel 就立刻写入 ZIP
		if err := WriteExcelToZip(zipWriter, fmt.Sprintf("%s.xlsx", componentID), data); err != nil {
			c.String(http.StatusInternalServerError, "导出失败: %v", err)
			return
		}
	}
}
go 复制代码
// WriteExcelToZip 将任意结构体切片数据导出为 Excel,并写入 zip 文件中
func WriteExcelToZip[T any](zipWriter *zip.Writer, filename string, data []T) error {
	// 创建 zip 条目
	entry, err := zipWriter.Create(filename)
	if err != nil {
		return err
	}

	// 创建 excel 文件
	f := excelize.NewFile()
	defer f.Close()

	sheet := "Sheet1"
	streamWriter, err := f.NewStreamWriter(sheet)
	if err != nil {
		return err
	}

	// 获取结构体字段作为表头
	t := reflect.TypeOf(data)
	if t.Kind() == reflect.Slice {
		t = t.Elem()
	}
	headers := []interface{}{}
	fields := []int{}
	for i := 0; i < t.NumField(); i++ {
		field := t.Field(i)
		tag := field.Tag.Get("excel")
		if tag == "" {
			tag = field.Name
		}
		headers = append(headers, tag)
		fields = append(fields, i)
	}
	if err := streamWriter.SetRow("A1", headers); err != nil {
		return err
	}

	// 写入数据行
	for i, v := range data {
		val := reflect.ValueOf(v)
		row := []interface{}{}
		for _, fi := range fields {
			row = append(row, val.Field(fi).Interface())
		}
		cell, _ := excelize.CoordinatesToCellName(1, i+2)
		if err := streamWriter.SetRow(cell, row); err != nil {
			return err
		}
	}

	// 刷新
	if err := streamWriter.Flush(); err != nil {
		return err
	}

	// 写入 zip entry
	_, err = f.WriteTo(entry)
	return err
}

5. 总结

以上就是excel web端导出的优化方法,其实还有一些优化空间,比如增加进度显示,支持http客户端中断等;但是总体来说,对于大数据量的导出,我们主要关注避免一次性将数据加载到内存,尽量使用分页查询和流式写入,同时要避免内存溢出和文件过大的问题。

相关推荐
江湖十年3 分钟前
go-multierror: 更方便的处理你的错误列表
后端·面试·go
谦行15 分钟前
前端视角 Java Web 入门手册 5.4:真实世界 Web 开发——Java Web 代码组织与分层
java·后端·架构
Goboy18 分钟前
构建异步消息通信机制设计与实现
后端·程序员·架构
汪小成28 分钟前
NestJS学习笔记-02-模块、控制器与服务,手把手构建你的第一个CRUD API!🚀
后端·nestjs
小周不摆烂1 小时前
解锁元生代:ComfyUI工作流与云原生后端的深度融合
后端
qq_447663051 小时前
Spring-注解编程
java·后端·spring
风象南1 小时前
Redis中5种BitMap应用场景及实现
redis·后端
声声codeGrandMaster3 小时前
Django之modelform使用
后端·python·django
慕容静漪9 小时前
如何本地安装Python Flask并结合内网穿透实现远程开发
开发语言·后端·golang
ErizJ10 小时前
Golang|锁相关
开发语言·后端·golang