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客户端中断等;但是总体来说,对于大数据量的导出,我们主要关注避免一次性将数据加载到内存,尽量使用分页查询和流式写入,同时要避免内存溢出和文件过大的问题。

相关推荐
姑苏洛言19 分钟前
搭建一款结合传统黄历功能的日历小程序
前端·javascript·后端
你的人类朋友22 分钟前
🍃认识一下boomi
后端
苏三说技术26 分钟前
MySQL的三大日志
后端
豌豆花下猫35 分钟前
让 Python 代码飙升330倍:从入门到精通的四种性能优化实践
后端·python·ai
南雨北斗1 小时前
TP6使用PHPMailer发送邮件
后端
你的人类朋友1 小时前
🤔什么时候用BFF架构?
前端·javascript·后端
争不过朝夕,又念着往昔3 小时前
Go语言反射机制详解
开发语言·后端·golang
绝无仅有4 小时前
企微审批对接错误与解决方案
后端·算法·架构
叹人间,美中不足今方信4 小时前
gRPC服务发现
rpc·go·服务发现
Code季风4 小时前
将 gRPC 服务注册到 Consul:从配置到服务发现的完整实践(上)
数据库·微服务·go·json·服务发现·consul