xlsx

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 3, 2026 License: MIT Imports: 16 Imported by: 0

README

xlsx

CI Go Reference Go Report Card

xlsx is a small, dependency-free Go library for creating Excel .xlsx workbooks incrementally. Rows are buffered only until you call CommitRows, then streamed to temporary files on disk, so memory usage stays flat even for very large spreadsheets.

Features

  • Streaming writer: commit rows in batches instead of holding a whole sheet in memory.
  • Keyed columns with an automatically generated header row.
  • Strings, booleans, integers, unsigned integers, floats, time.Time, []byte, and database/sql/driver.Valuer values (including sql.Null*).
  • Correct XML escaping, date serials and cell references.
  • Multi-sheet workbooks, column widths, deterministic XML, and no third-party dependencies.
  • Tested on Linux, macOS, and Windows with Go 1.24, 1.25, and 1.26.

Installation

go get github.com/zignd/xlsx@latest

Requires Go 1.24 or newer.

Quick start

package main

import (
	"log"

	"github.com/zignd/xlsx"
)

func main() {
	workbook := xlsx.NewWorkbook("report.xlsx")
	worksheet := workbook.AddWorksheet(&xlsx.WorksheetOptions{Name: "Sales"})

	for i := 0; i < 1_000_000; i++ {
		row, err := worksheet.AddRow()
		if err != nil {
			log.Fatal(err)
		}

		product, err := row.AddCell()
		if err != nil {
			log.Fatal(err)
		}
		product.Value = "Keyboard"

		amount, err := row.AddCell()
		if err != nil {
			log.Fatal(err)
		}
		amount.Value = 49.90

		// Flush this batch to disk. Repeat CommitRows to keep memory flat.
		if err := worksheet.CommitRows(); err != nil {
			log.Fatal(err)
		}
	}

	// Worksheets without pending rows are finalized automatically.
	if err := workbook.Commit(); err != nil {
		log.Fatal(err)
	}
}

CommitRows can be called once per row (as above) or once per batch. Calling it per batch is usually faster because it amortizes the append/flush work:

const batchSize = 10_000

for i := 0; i < total; i++ {
	row, _ := worksheet.AddRow()
	// ... add cells ...
	if (i+1)%batchSize == 0 {
		if err := worksheet.CommitRows(); err != nil {
			log.Fatal(err)
		}
	}
}
if err := worksheet.CommitRows(); err != nil { // flush the final partial batch
	log.Fatal(err)
}
if err := workbook.Commit(); err != nil {
	log.Fatal(err)
}

Keyed columns

Defining columns writes a header row and lets you address cells by key. Missing cells are written as blank cells so column positions stay stable.

worksheet := workbook.AddWorksheet(&xlsx.WorksheetOptions{Name: "Sales"})

err := worksheet.DefineColumns([]*xlsx.WorksheetColumn{
	{Key: "product", Value: "Product", Width: 30},
	{Key: "amount", Value: "Amount"},
	{Key: "sold_at", Value: "Sold at", Width: 22},
})
if err != nil {
	log.Fatal(err)
}

row, err := worksheet.AddRow()
if err != nil {
	log.Fatal(err)
}

product, err := row.AddCellWithKey("product")
if err != nil {
	log.Fatal(err)
}
product.Value = "Keyboard"

amount, err := row.AddCellWithKey("amount")
if err != nil {
	log.Fatal(err)
}
amount.Value = 49.90

if err := worksheet.CommitRows(); err != nil {
	log.Fatal(err)
}
if err := workbook.Commit(); err != nil {
	log.Fatal(err)
}

Supported values

Go type Written as
string, []byte, named string and byte-slice types Inline string
bool, named bool types Excel boolean
Signed and unsigned integers Number
float32, float64 Number
time.Time Excel date serial with a yyyy-mm-dd hh:mm:ss style
database/sql/driver.Valuer The value returned by Value()
nil Blank cell

Unsupported values (structs other than time.Time, maps, NaN, infinities, and so on) return an error from CommitRows, wrapped with ErrUnsupportedType.

Dates are written using their wall-clock components (the location is ignored), which is how Excel stores date-times. The 1900 date system is used.

API overview

func NewWorkbook(filePath string) *Workbook

func (wb *Workbook) AddWorksheet(opts *WorksheetOptions) *Worksheet
func (wb *Workbook) Commit() error
func (wb *Workbook) Cleanup() error
func (wb *Workbook) HasPendingWorksheets() bool
func (wb *Workbook) Worksheets() []*Worksheet

func (ws *Worksheet) DefineColumns(columns []*WorksheetColumn) error
func (ws *Worksheet) AddRow() (*Row, error)
func (ws *Worksheet) CommitRows() error
func (ws *Worksheet) Commit() error

func (r *Row) AddCell() (*Cell, error)
func (r *Row) AddCellWithKey(key string) (*Cell, error)

Errors that callers are expected to handle are exported as sentinels and can be checked with errors.Is:

if err := workbook.Commit(); errors.Is(err, xlsx.ErrPendingRows) {
	// some worksheet still has rows waiting to be committed
}

See the package documentation for the complete API and examples.

Design notes and limitations

  • The library is a writer only; it does not read XLSX files.
  • A worksheet is written to a temporary directory while rows are appended, then packaged into a ZIP archive when Workbook.Commit runs. The temporary files are removed automatically after a successful commit; call Cleanup if you abandon a workbook.
  • Workbook.Commit finalizes worksheets that have no pending rows, but it does not flush pending rows for you. Call CommitRows first.
  • Strings are always written as inline strings. Formulas and rich text are not supported.
  • Workbook, Worksheet, and Row are not safe for concurrent use. Use one workbook per goroutine; independent workbooks can be written concurrently.
  • Worksheet names are validated when the workbook is committed (31 characters maximum, no []:*?/\\, no leading/trailing apostrophe, unique, case-insensitive).
  • Excel's hard limits are enforced: 1,048,576 rows and 16,384 columns per worksheet.

Benchmarks

Run the benchmarks with:

make bench
# or
go test -run '^$' -bench . -benchmem ./...

The suite covers cell-reference generation, XML escaping, date conversion, row buffering, CommitRows, and full workbook commits. Results depend on the machine and storage; the benchmark smoke test in CI only verifies that they compile and run.

Development

make test       # go test ./...
make test-race  # race detector
make cover      # coverage profile + function report
make bench      # benchmarks
make lint       # golangci-lint
make check      # tidy + format + vet + race tests + lint

CI runs the test matrix, lint, module-tidiness, benchmark, and vulnerability checks on every push and pull request. Dependabot keeps the Go module and GitHub Actions up to date.

Versioning and releases

The project follows Semantic Versioning. Versions are regular Go module tags, so they are published to pkg.go.dev automatically.

To release from the GitHub UI, run the Tag release workflow and choose patch, minor, or major (optionally with a prerelease suffix), or provide an exact tag such as v1.2.3. The workflow runs the tests, computes or validates the tag, creates an annotated tag, and pushes it. The tag then triggers Release, which validates the SemVer tag, builds, tests, scans for vulnerabilities, and creates the GitHub release with generated notes.

The same flow can be driven locally:

git tag -a v1.2.3 -m "Release v1.2.3"
git push origin v1.2.3

License

MIT. See LICENSE.

Documentation

Overview

Package xlsx lets you create XLSX files incrementally.

Rows are buffered only until CommitRows is called and are then streamed to temporary files on disk. This keeps memory usage low even when writing very large spreadsheets.

Quick start

workbook := xlsx.NewWorkbook("report.xlsx")
worksheet := workbook.AddWorksheet(&xlsx.WorksheetOptions{Name: "Sales"})

row, err := worksheet.AddRow()
if err != nil {
	log.Fatal(err)
}
cell, err := row.AddCell()
if err != nil {
	log.Fatal(err)
}
cell.Value = "Revenue"

if err := worksheet.CommitRows(); err != nil {
	log.Fatal(err)
}
if err := worksheet.Commit(); err != nil {
	log.Fatal(err)
}
if err := workbook.Commit(); err != nil {
	log.Fatal(err)
}

Values

Cell.Value accepts strings, booleans, all integer and unsigned integer types, floats, time.Time, []byte and database/sql/driver.Valuer implementations (such as the sql.Null* types). Unsupported values are reported when rows are committed.

Streaming

Call Worksheet.CommitRows after adding a batch of rows to release the rows from memory. The worksheet keeps a buffered writer open, so repeated batches are cheap to append.

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrWorkbookCommitted is returned when an operation is attempted on a
	// workbook that has already been committed.
	ErrWorkbookCommitted = errors.New("workbook already committed")

	// ErrWorksheetCommitted is returned when an operation is attempted on a
	// worksheet that has already been committed.
	ErrWorksheetCommitted = errors.New("worksheet already committed")

	// ErrNoWorksheets is returned when a workbook without worksheets is
	// committed.
	ErrNoWorksheets = errors.New("a workbook needs at least one worksheet")

	// ErrNoRows is returned when CommitRows is called without pending rows.
	ErrNoRows = errors.New("there are no rows to commit")

	// ErrPendingRows is returned when a worksheet is committed while it still
	// has rows waiting to be written.
	ErrPendingRows = errors.New("worksheet has pending rows")

	// ErrNoColumns is returned when a keyed cell is added to a worksheet that
	// has no columns defined.
	ErrNoColumns = errors.New("no columns were defined on the worksheet")

	// ErrColumnsDefined is returned when an unkeyed cell is added to a
	// worksheet that uses keyed columns.
	ErrColumnsDefined = errors.New("columns were defined on the worksheet")

	// ErrUnsupportedType is returned when a cell value cannot be represented
	// in an XLSX file.
	ErrUnsupportedType = errors.New("unsupported cell value type")

	// ErrInvalidSheetName is returned when a worksheet name is not a valid
	// Excel sheet name.
	ErrInvalidSheetName = errors.New("invalid worksheet name")

	// ErrDuplicateSheetName is returned when two worksheets share a name.
	ErrDuplicateSheetName = errors.New("duplicate worksheet name")

	// ErrInvalidColumn is returned when a column definition is invalid.
	ErrInvalidColumn = errors.New("invalid worksheet column")
)

Sentinel errors returned by the package. They can be inspected with errors.Is.

Functions

This section is empty.

Types

type Cell

type Cell struct {

	// Key is the column key used to create the cell, if any.
	Key string
	// Value is the cell value. It is written when the owning row is committed.
	Value interface{}
	// contains filtered or unexported fields
}

Cell represents a cell in a worksheet. A row has a collection of cells.

func (*Cell) Identifier

func (c *Cell) Identifier() string

Identifier returns the A1-style reference of the cell (for example "B3").

func (*Cell) Index

func (c *Cell) Index() int

Index returns the zero-based column index of the cell.

func (*Cell) Row

func (c *Cell) Row() *Row

Row returns the row that owns the cell.

type CellOptions

type CellOptions struct {
	// Key is the optional column key.
	Key *string
	// Value is the initial cell value.
	Value interface{}
}

CellOptions has options used when creating a new cell.

type Row

type Row struct {
	// contains filtered or unexported fields
}

Row represents a row in a worksheet. A worksheet has a collection of rows.

func (*Row) AddCell

func (r *Row) AddCell() (*Cell, error)

AddCell adds a new cell to the row. It can only be used on worksheets where columns were not defined.

func (*Row) AddCellWithKey

func (r *Row) AddCellWithKey(key string) (*Cell, error)

AddCellWithKey is just like AddCell but adds a key to the cell. It should be used when columns were defined on the worksheet.

func (*Row) Index

func (r *Row) Index() int

Index returns the zero-based index of the row.

type Workbook

type Workbook struct {
	// FilePath is the destination of the XLSX file written by Commit.
	FilePath string
	// contains filtered or unexported fields
}

Workbook represents a spreadsheet workbook.

func NewWorkbook

func NewWorkbook(filePath string) *Workbook

NewWorkbook creates a new workbook, which is the base for every XLSX file. The file is written when Commit is called.

Example
package main

import (
	"fmt"
	"os"
	"path/filepath"

	"github.com/zignd/xlsx"
)

func main() {
	dir, err := os.MkdirTemp("", "xlsx-example-*")
	if err != nil {
		panic(err)
	}
	defer os.RemoveAll(dir)

	workbook := xlsx.NewWorkbook(filepath.Join(dir, "report.xlsx"))
	worksheet := workbook.AddWorksheet(&xlsx.WorksheetOptions{Name: "Sales"})

	for i := 0; i < 3; i++ {
		row, err := worksheet.AddRow()
		if err != nil {
			panic(err)
		}

		product, err := row.AddCell()
		if err != nil {
			panic(err)
		}
		product.Value = fmt.Sprintf("Product %d", i+1)

		price, err := row.AddCell()
		if err != nil {
			panic(err)
		}
		price.Value = 9.99 * float64(i+1)

		if err := worksheet.CommitRows(); err != nil {
			panic(err)
		}
	}

	if err := workbook.Commit(); err != nil {
		panic(err)
	}

	fmt.Println("created report.xlsx with 3 rows")
}
Output:
created report.xlsx with 3 rows

func (*Workbook) AddWorksheet

func (wb *Workbook) AddWorksheet(opts *WorksheetOptions) *Worksheet

AddWorksheet adds a new worksheet to the workbook.

Invalid or duplicate worksheet names are reported when the worksheet (or the workbook) is committed, because this method intentionally keeps the original error-free API.

func (*Workbook) Cleanup

func (wb *Workbook) Cleanup() error

Cleanup removes the temporary files used to build the workbook. It is safe to call multiple times and is called automatically after a successful Commit.

func (*Workbook) Commit

func (wb *Workbook) Commit() error

Commit commits the workbook, persisting the data to the file path supplied to NewWorkbook. Worksheets with no pending rows are finalized automatically.

func (*Workbook) HasPendingWorksheets

func (wb *Workbook) HasPendingWorksheets() bool

HasPendingWorksheets indicates whether the workbook has worksheets that have not been finalized yet.

func (*Workbook) Worksheets

func (wb *Workbook) Worksheets() []*Worksheet

Worksheets returns a copy of the workbook's worksheet slice.

type Worksheet

type Worksheet struct {
	// contains filtered or unexported fields
}

Worksheet represents a worksheet in a workbook.

func (*Worksheet) AddRow

func (ws *Worksheet) AddRow() (*Row, error)

AddRow adds a new row to the worksheet. Rows are buffered in memory until CommitRows is called.

func (*Worksheet) Commit

func (ws *Worksheet) Commit() error

Commit commits the worksheet, writing its footer and closing the underlying file. Workbook.Commit finalizes worksheets automatically; calling Commit on an already committed worksheet returns ErrWorksheetCommitted.

func (*Worksheet) CommitRows

func (ws *Worksheet) CommitRows() error

CommitRows writes the rows stored in memory to disk. It can be called repeatedly to stream data without keeping the whole worksheet in memory.

func (*Worksheet) DefineColumns

func (ws *Worksheet) DefineColumns(columns []*WorksheetColumn) error

DefineColumns defines the worksheet columns and writes the header row. It is optional; a worksheet can be used without columns by calling AddCell.

Example
package main

import (
	"fmt"
	"os"
	"path/filepath"

	"github.com/zignd/xlsx"
)

func main() {
	dir, err := os.MkdirTemp("", "xlsx-example-*")
	if err != nil {
		panic(err)
	}
	defer os.RemoveAll(dir)

	workbook := xlsx.NewWorkbook(filepath.Join(dir, "report.xlsx"))
	worksheet := workbook.AddWorksheet(&xlsx.WorksheetOptions{Name: "Sales"})

	err = worksheet.DefineColumns([]*xlsx.WorksheetColumn{
		{Key: "product", Value: "Product", Width: 30},
		{Key: "amount", Value: "Amount"},
	})
	if err != nil {
		panic(err)
	}

	row, err := worksheet.AddRow()
	if err != nil {
		panic(err)
	}
	product, err := row.AddCellWithKey("product")
	if err != nil {
		panic(err)
	}
	product.Value = "Keyboard"
	amount, err := row.AddCellWithKey("amount")
	if err != nil {
		panic(err)
	}
	amount.Value = 49.9

	if err := worksheet.CommitRows(); err != nil {
		panic(err)
	}
	if err := workbook.Commit(); err != nil {
		panic(err)
	}

	fmt.Println("created a keyed worksheet")
}
Output:
created a keyed worksheet

func (*Worksheet) FileName

func (ws *Worksheet) FileName() string

FileName returns the name of the worksheet part inside the XLSX package.

func (*Worksheet) ID

func (ws *Worksheet) ID() int

ID returns the one-based identifier of the worksheet.

func (*Worksheet) Name

func (ws *Worksheet) Name() string

Name returns the worksheet name.

type WorksheetColumn

type WorksheetColumn struct {
	// Key identifies the column when calling AddCellWithKey.
	Key string
	// Value is the header text written in the first row.
	Value string
	// Width is the optional column width in Excel units. A value of zero
	// leaves the column at its default width.
	Width float64
}

WorksheetColumn represents a column in a worksheet.

type WorksheetOptions

type WorksheetOptions struct {
	// Name is the worksheet name shown in Excel.
	Name string
}

WorksheetOptions has options used when creating a new worksheet.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL