😊

Goのexcelize.StreamWriterを使ったらExcelファイルの出力が速くなった

2023/06/16に公開

はじめに

株式会社CastingONEでバックエンドエンジニアをしている村上です。

最近のマイブームは育爪です。
こちらのネイルオイルのおかげで、なんかいい感じになってる気がします。

excelize.StreamWriter

弊社ではバックエンドでExcelファイルを生成するために、Goのexcelizeを使っています。セルに値を出力する際、いままでFile.SetCellValueを使ってきましたが、StreamWriterというものがあることに気づきました。

v2.1.0で追加されたようで、下記のようにパフォーマンスが向上しています。

New functions NewStreamWriter and Flush to generate the new worksheet with huge amounts of data.
Compared to non-streaming writing, reduced memory usage by 90.2%, time cost by 53%

https://github.com/qax-os/excelize/releases/tag/v2.1.0

サイズの大きいファイルを出力しようとすると、時間がかかったりメモリ不足のエラーが発生したりしていたので、StreamWriterに乗り換えたところパフォーマンスをかなり改善できました。

備忘録をかねて、ベンチマークを残しておきたいと思います。

ベンチマーク

StreamWriterを使わない関数と使う関数を書いて、

package main

import (
	"log"

	"github.com/xuri/excelize/v2"
)

func NonStreaming() {
	file := excelize.NewFile()
	defer func() {
		if err := file.Close(); err != nil {
			log.Fatal(err)
		}
	}()

	sheetName := "Test"
	_, err := file.NewSheet(sheetName)
	if err != nil {
		log.Fatal(err)
	}

	for row := 1; row <= 100; row++ {
		for col := 1; col <= 100; col++ {
			cell, err := excelize.CoordinatesToCellName(col, row)
			if err != nil {
				log.Fatal(err)
			}

			err = file.SetCellValue(sheetName, cell, row+col)
			if err != nil {
				log.Fatal(err)
			}
		}
	}

	if err := file.SaveAs("NonStreaming.xlsx"); err != nil {
		log.Fatal(err)
	}
}

func Streaming() {
	file := excelize.NewFile()
	defer func() {
		if err := file.Close(); err != nil {
			log.Fatal(err)
		}
	}()

	sheetName := "Test"
	_, err := file.NewSheet(sheetName)
	if err != nil {
		log.Fatal(err)
	}

	sw, err := file.NewStreamWriter(sheetName)
	if err != nil {
		log.Fatal(err)
	}

	for row := 1; row <= 100; row++ {
		values := []interface{}{}
		for col := 1; col <= 100; col++ {
			values = append(values, row+col)
		}

		cell, err := excelize.CoordinatesToCellName(1, row)
		if err != nil {
			log.Fatal(err)
		}
		err = sw.SetRow(cell, values)
		if err != nil {
			log.Fatal(err)
		}
	}

	if err := sw.Flush(); err != nil {
		log.Fatal(err)
	}
	if err := file.SaveAs("Streaming.xlsx"); err != nil {
		log.Fatal(err)
	}
}

ベンチマークを取るコードも書きました。

package main

import "testing"

func BenchmarkNonStreaming(b *testing.B) {
	for i := 0; i < b.N; i++ {
		NonStreaming()
	}
}

func BenchmarkStreaming(b *testing.B) {
	for i := 0; i < b.N; i++ {
		Streaming()
	}
}

StreamWriterを使った関数の方が、高速でメモリ効率も良い結果となりました。出力する値によって結果が異なる可能性もありますが、StreamWriterかなり良いと思いました!

$ go test -bench . -benchmem
goos: darwin
goarch: arm64
pkg: go-excelize
BenchmarkNonStreaming-8               75          15306014 ns/op         5927855 B/op      94783 allocs/op
BenchmarkStreaming-8                 218           5791354 ns/op         1919249 B/op      48058 allocs/op
PASS
ok      go-excelize     3.789s

デメリットとして、従来のFileは SetCellStyleSetColStyleSetRowStyleといった書式を設定するメソッドが充実していますが、StreamWriterにはそれらが揃っていないところがあります。今後機能が増えていくにつれて、さらに使いやすくなると思います!

おわりに

弊社ではいっしょに働いてくれるエンジニアを募集中です。社員でもフリーランスでも、フルタイムでも短時間の副業でも大歓迎なので、気軽にご連絡ください!

https://www.wantedly.com/projects/1130967

Discussion