golang 10w数据流式导出excel并且记录进度条

前序

  1. 流式导出excel网上都有
  2. 并发写入的话需要保证写入的顺序性(A1,A2,A3……)
  3. 所以网上的大部分都是一个循环获取数据,写入数据
  4. 我这里获取数据和写入数据做了拆分,引入缓冲层
  5. 我的这包直接引入项目就可以使用了
  6. progress是进度条的库

代码如下

util.go

package util

import (
	"context"

	"github.com/gin-gonic/gin"
	"github.com/spf13/cast"
)

// CheckCanceled 检查上下文是否已取消
func CheckCanceled(ctx context.Context) bool {
	select {
	case <-ctx.Done():
		return true // 上下文已取消
	default:
		return false // 上下文未取消
	}
}

excel_export.go

package excel_export

import (
	"context"
	"fmt"
	"time"

	"xxx/dmp/demo/common/progress"
	"xxx/dmp/demo/util"
	"github.com/gin-gonic/gin"
	"github.com/google/uuid"
	"github.com/xuri/excelize/v2"
)

const (
	defaultColWidth        = 20.0
	defaultRowHeight       = 17.5
	defaultSheetName       = "Sheet1"
	defaultHeaderStyleSize = 14
	defaultNormalStyleSize = 12
	ExportDir              = "tmp/export"
	ExpireDuration         = time.Hour
)

type ExcelExporterImpl interface {
	Total() (int64, error)
	Headers() []interface{}
	GetData(offset int, limit int) ([][]interface{}, error)
}

type ExcelExportBuilder struct {
	file       *excelize.File
	ProgressTx string // 进度条
	ctx        *gin.Context
}

func init() {
	// 清理文件
	go func() {
		for range time.Tick(time.Minute * 5) {
			util.CleanOldFiles(ExportDir, time.Hour)
		}
	}()
}

func GetExcelExportFilePath(filename string) string {
	return fmt.Sprintf("%s/%s-%s.xlsx", ExportDir, filename, time.Now().Format("200601021504"))
}

func NewExcelExportBuilder(ctx *gin.Context, filepath string) (*ExcelExportBuilder, error) {
	f, width, height := excelize.NewFile(), defaultColWidth, defaultRowHeight
	f.Path = filepath

	if err := f.SetSheetProps(defaultSheetName, &excelize.SheetPropsOptions{
		DefaultColWidth:  &width,
		DefaultRowHeight: &height,
	}); err != nil {
		return nil, err
	}

	return &ExcelExportBuilder{
		file:       f,
		ProgressTx: uuid.New().String(),
		ctx:        ctx,
	}, nil
}

func (builder *ExcelExportBuilder) Save(impl ExcelExporterImpl) error {
	// 关闭文件
	defer builder.file.Close()
	// 打开sheet1
	sw, err := builder.file.NewStreamWriter("Sheet1")
	if err != nil {
		return err
	}
	// 表头样式
	headerStyleID, err := builder.file.NewStyle(&excelize.Style{Font: &excelize.Font{Bold: true, Size: defaultHeaderStyleSize}})
	if err != nil {
		return err
	}
	// 这里设置表头
	headers := impl.Headers()
	err = sw.SetRow("A1", headers, excelize.RowOpts{StyleID: headerStyleID})
	if err != nil {
		return err
	}
	// 获取总量
	totalRows, err := impl.Total()
	if err != nil {
		return err
	}
	// 进度条
	progre := progress.NewProgress(builder.ctx, builder.ProgressTx, totalRows)
	// 创建一个父上下文用于协程之间的协作
	ctx, cancel := context.WithCancel(context.Background())
	var errCtx error
	defer cancel()
	// 并发写入数据
	var batchSize int64 = 1000
	var offset int64
	// 创建一个带有缓冲的通道,用于获取数据缓冲
	type ExportBuffer struct {
		data   [][]interface{}
		offset int64
	}
	// 并发获取数据 --> 放入队列 --> 保证写入数据时顺序 --> 这里是buffer层
	bufferCh := make(chan ExportBuffer, 10) // 这里设置适当的缓冲大小
	go func() {
		defer close(bufferCh) // 关闭通道表示数据获取完成
		for offset < totalRows {
			// 检查上下文是否已取消
			if util.CheckCanceled(ctx) {
				return
			}
			data, err := impl.GetData(int(offset), int(batchSize))
			if err != nil {
				cancel()
				errCtx = err
				return
			}
			bufferCh <- ExportBuffer{
				data:   data,
				offset: offset,
			}
			offset += batchSize
		}
	}()

	// 循环缓冲写入数据
	for buffer := range bufferCh {
		// fmt.Println("buffer.offset:", buffer.offset)
		for index, row := range buffer.data {
			line := buffer.offset + int64(index) + 2
			err := sw.SetRow(fmt.Sprintf("A%d", line), row)
			if err != nil {
				fmt.Println(err)
			}
			// 写入进度
			progre.AddSuccessNumber(1)
		}
	}

	// 检查上下文是否已取消
	if util.CheckCanceled(ctx) {
		progre.SetFail(errCtx.Error())
		return errCtx
	}

	// 写入进度 100%
	progre.SetProgress(100)

	if err = sw.Flush(); err != nil {
		return err
	}

	return builder.file.Save()
}

进度条
progress.go

package progress

import (
	"encoding/json"
	"fmt"
	"sync"
	"time"

	"xxxx/dmp/demo/common/global"
	"xxxx/dmp/demo/util"
	"github.com/gin-gonic/gin"
)

const (
	ProgressInit    = 0
	ProgressIn      = 1
	ProgressSuccess = 2
	ProgressFail    = 3
)
const ProgressRedisExp = 3600
const ProgressGap = 5 // 存储间隙 就是多少个百分点存储 目的减少redis set
const (
	ProgressTypeFileImport = 1
	ProgressTypeFileExport = 2
	ProgressTypeBuild      = 3
)

type Progress struct {
	ProgressTx     string                 `json:"progress_tx"`     // 可以理解唯一id
	Percentage     float64                `json:"percentage"`      // 当前进度百分比
	Status         int                    `json:"status"`          // 状态
	Message        string                 `json:"message"`         // 描述
	PrevPercentage float64                `json:"prev_percentage"` // 上次百分比
	TotalNumber    int64                  `json:"total_number"`    // 总数量
	SuccessNumber  int64                  `json:"success_number"`  // 完成数量
	Type           int                    `json:"type"`            // 数据类型
	Data           map[string]interface{} `json:"data"`            // 数据
	FilePath       string                 `json:"filePath"`        // 文件下载路径  --> 导出接口才有
	// redisConn           redis.Conn // 没有采用一条连接处理是怕new多了连接池被打满了
	redisLock           sync.Mutex
	progressLock        sync.Mutex
	addSuccessNumberQue chan int64
	closed              bool
	ctx                 *gin.Context
}

func GetProgressRedisKey(key string) string {
	return fmt.Sprintf("progress:%s", key)
}

// 传入上下文 -> 多租户的情况下需要上下文取租户号处理
func NewProgress(c *gin.Context, progressTx string, total int64) *Progress {
	// ctx := context.
	progress := &Progress{
		ProgressTx:  progressTx,
		TotalNumber: total,
		// redisConn:           global.REDISPoll.Get(),
		addSuccessNumberQue: make(chan int64, 100),
		ctx:                 c,
	}
	// 定时存档和保活
	prevSaveTime := time.Now()
	go func(p *Progress) {
		// 存档和redis连接保活
		ticker := time.NewTicker(time.Second * 10)
		// defer p.redisConn.Close()
		defer ticker.Stop()
		defer p.closeAddSuccessNumberQue()
		for range ticker.C {
			// redis存档
			p.save()
			// 判断状态是否成功或者进度卡了1个小时不动了就释放
			if p.IsFinalState() || time.Since(prevSaveTime) > time.Hour {
				// fmt.Println("????????????????????")
				return
			}
		}
	}(progress)
	// 进度条处理
	go func(p *Progress) {
		for num := range p.addSuccessNumberQue {
			p.SuccessNumber += num
			// 设置进度条
			percentage := float64(p.SuccessNumber*10000/p.TotalNumber) / 100
			p.SetProgress(percentage)
			// 存档保活
			prevSaveTime = time.Now()
			// fmt.Println(p.TotalNumber, "  ", p.SuccessNumber, "  ", percentage)
		}
	}(progress)
	return progress
}

// 关闭通道 必须上锁有协程
func (p *Progress) closeAddSuccessNumberQue() {
	// fmt.Println("关闭通道")
	p.progressLock.Lock()
	defer p.progressLock.Unlock()
	if !p.closed {
		p.closed = true
		close(p.addSuccessNumberQue)
		// 处理通道内数据
		go func() {
			for range p.addSuccessNumberQue {
			}
		}()
	}
}

func (p *Progress) IsFinalState() bool {
	// 判断最终状态
	return p.Status == ProgressSuccess || p.Status == ProgressFail
}

// 获取进度条数据
func GetProgress(c *gin.Context, progressTx string) (Progress, error) {
	return util.GetSetAny[Progress](GetProgressRedisKey(progressTx), func(item util.RedisItemInterface) (Progress, error) {
		item.ExpiresAfter(ProgressRedisExp)
		return Progress{
			ProgressTx: progressTx,
			Percentage: 0,
		}, nil
	})
}

// 追加完成数量
func (p *Progress) AddSuccessNumber(num int64) {
	if !p.closed {
		p.addSuccessNumberQue <- num
	}
}

func (p *Progress) SetTotalNumber(num int64) {
	if !p.closed {
		p.TotalNumber = num
	}
}

// 设置当前进度 --> 这个方法不要并发调用
func (p *Progress) SetProgress(percentage float64) error {
	if p.IsFinalState() {
		// 终状态不处理
		return nil
	}

	p.Status = ProgressIn
	if percentage >= 100 {
		percentage = 100
		p.Message = "SUCCESS"
		p.SuccessNumber = p.TotalNumber
		p.Status = ProgressSuccess
		p.closeAddSuccessNumberQue()
	}
	if percentage-p.PrevPercentage > 0 {
		p.Percentage = percentage
	}
	// 判断上次和当前次间隙
	if percentage-p.PrevPercentage >= ProgressGap || percentage == 100 {
		// fmt.Println(p.ProgressTx, "     PrevPercentage:", p.PrevPercentage, "   percentage:", percentage)
		p.save()
		p.PrevPercentage = percentage
	}

	return nil
}

// 失败处理
func (p *Progress) SetFail(message string) error {
	if p.IsFinalState() {
		// 终状态不处理
		return nil
	}
	p.Status = ProgressFail
	p.Message = message
	p.closeAddSuccessNumberQue()
	p.save()
	return nil
}

// 设置文件下载路径
func (p *Progress) SetFilePath(filepath string) error {
	p.FilePath = filepath
	p.save()
	return nil
}

// 设置数据
func (p *Progress) SetData(typ int, data map[string]interface{}, message string) error {
	p.Type = typ
	p.Data = data
	p.Message = message
	p.save()
	return nil
}

// 存档
func (p *Progress) save() error {
	p.redisLock.Lock()
	defer p.redisLock.Unlock()
	progressStr, err := json.Marshal(p)
	if err != nil {
		return err
	}
	conn := global.REDISPoll.Get()
	defer conn.Close()
	_, err = conn.Do("set", GetProgressRedisKey(p.ProgressTx), progressStr, "EX", ProgressRedisExp)
	return err
}


实现导出接口
service.go

type whiteExport struct {
	req *request.ExportWhiteRequest
	c   *gin.Context
}

// 获取总量
func (w *whiteExport) Total() (int64, error) {
	return model.GetWhiteTotal(w.req.PublicWhite)
}

// 表头
func (w *whiteExport) Headers() []any {
	return []any{"名称"}
}

// 表内容
func (w *whiteExport) GetData(offset int, limit int) ([][]interface{}, error) {
	// 获取数据
	data, err := model.GetData()
	if err != nil {
		return nil, err
	}
	result := make([][]interface{}, len(data))
	for i := 0; i < len(data); i++ {
		result[i] = []interface{}{
			data[i].WhiteName,
		}
	}
	return result, nil
}

func ExportWhiteExcel(c *gin.Context, req *request.ExportWhiteRequest) (*response.ExportResp, error) {
	filepath := excel_export.GetExcelExportFilePath("测试")
	whiteExport := &whiteExport{
		c:   c,
		req: req,
	}
	builder, err := excel_export.NewExcelExportBuilder(c, filepath)
	if err != nil {
		return nil, err
	}

	err = builder.Save(whiteExport)
	if err != nil {
		return nil, err
	}

	return &response.ExportResp{
		Download:   filepath,
		ProgressTx: builder.ProgressTx,
	}, nil
}

csv的我没有封装以后可能封装起来

CSV看这篇 CSV封装代码

https://blog.csdn.net/qq_39272466/article/details/131663379?spm=1001.2014.3001.5501

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 在Golang中使用流读取超大Excel文件可以使用第三方库"github.com/360EntSecGroup-Skylar/excelize"。该库支持流读取Excel文件,可以解决内存占用过大的问题。使用时,需要先打开Excel文件,然后使用GetRows方法读取每一行的数据。 ### 回答2: 要使用流读取超大的Excel文件,可以使用Go语言的Ole库,该库提供了对OLE2文件格的访问。 首先,需要安装Go语言的Ole库。可以通过以下命令进行安装: ``` go get github.com/go-ole/go-ole ``` 然后,导入Ole库并创建一个OLE reader对象。可以使用如下代码: ```go import ( "github.com/go-ole/go-ole" "github.com/go-ole/go-ole/oleutil" ) func main() { ole.CoInitialize(0) defer ole.CoUninitialize() unknown, _ := oleutil.CreateObject("excel.Application") excel, _ := unknown.QueryInterface(ole.IID_IDispatch) defer excel.Release() workbooks, _ := oleutil.GetProperty(excel, "Workbooks") defer workbooks.Clear() workbook, _ := oleutil.CallMethod(workbooks.ToIDispatch(), "Open", "path/to/file.xlsx") defer workbook.Clear() sheets, _ := oleutil.GetProperty(workbook.ToIDispatch(), "Worksheets") defer sheets.Clear() sheet, _ := oleutil.GetProperty(sheets.ToIDispatch(), "Item", 1) defer sheet.Clear() rows, _ := oleutil.GetProperty(sheet.ToIDispatch(), "Rows") defer rows.Clear() count, _ := rows.GetProperty("Count") rowCount, _ := count.Value().(int) for i := 1; i <= rowCount; i++ { row, _ := oleutil.GetProperty(rows.ToIDispatch(), "Item", i) defer row.Clear() cells, _ := oleutil.GetProperty(row.ToIDispatch(), "Cells") defer cells.Clear() // 获取单元格数据 cell1, _ := oleutil.GetProperty(cells.ToIDispatch(), "Item", 1) defer cell1.Clear() cell2, _ := oleutil.GetProperty(cells.ToIDispatch(), "Item", 2) defer cell2.Clear() // 处理单元格数据 value1 := cell1.Value() value2 := cell2.Value() // TODO: 处理单元格数据 } } ``` 上述代码通过OLE库创建了一个Excel对象,并打开了指定的Excel文件。然后,它获取了第一个工作表,并遍历了每一行的数据。通过调用`GetProperty`和`CallMethod`方法,可以获取单元格的值。你可以根据自己的需求,处理这些单元格的数据。 需要注意的是,对于大型Excel文件,可能会遇到性能问题。可以通过在循环中添加一些优化策略来提高性能,如批量读取多行数据,避免重复的COM调用等。 希望这个回答能够帮助到你。 ### 回答3: 在Golang中,可以使用excelize库来读取超大的Excel文件。该库提供了流读取的功能,可以有效地处理超大的Excel文件,而不会导致内存溢出。 首先,需要使用excelize库的OpenReader方法,以只读模打开Excel文件。然后,可以使用GetSheetNameList方法获取所有的Sheet名称。接下来,可以使用GetRows方法逐行读取指定的Sheet。该方法返回一个二维数组,每一行都是一个字符串切片,表示该行的数据。 为了避免一次性读取所有数据导致的内存溢出,可以使用for循环逐行读取数据,并在每次循环结束后将内存中的数据清空。这样,就可以实现流读取超大Excel文件的效果。 下面是一个简单的示例代码,用于读取Excel文件的第一个Sheet中的数据: ```go package main import ( "fmt" "github.com/360EntSecGroup-Skylar/excelize" ) func main() { xlsx, err := excelize.OpenFile("example.xlsx") if err != nil { fmt.Println(err) return } // 获取所有的Sheet名称 sheetNameList := xlsx.GetSheetList() // 读取第一个Sheet中的数据 rows, err := xlsx.GetRows(sheetNameList[0]) if err != nil { fmt.Println(err) return } for _, row := range rows { // 处理每一行的数据 // ... // 释放内存 row = nil } // 释放内存 rows = nil } ``` 通过以上方法,就可以实现Golang中对超大Excel文件的流读取。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值