ZetCode

Go Excel

last modified October 30, 2026

In this article we show how to read and write Excel files in Go with excelize. The library works with the Office Open XML formats .xlsx, .xlsm and .xltm; the older binary .xls format is not supported.

The examples were compiled and run with Go 1.27.0 and excelize v2.11.0. The generated files were checked with the file command and by reading the values back with the same library.

Installation

$ mkdir excel-demo
$ cd excel-demo
$ go mod init com.zetcode/excel
go: creating new go.mod: module com.zetcode/excel
$ go get github.com/xuri/excelize/v2@v2.11.0 2>&1 | grep excelize
go: added github.com/xuri/excelize/v2 v2.11.0

Creating a workbook

excelize.NewFile creates a workbook with one sheet called Sheet1. SetCellValue accepts numbers, strings, booleans and times, and SaveAs writes the file.

main.go
package main

import (
    "fmt"
    "log"

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

func main() {

    f := excelize.NewFile()
    defer f.Close()

    values := map[string]any{
        "A1": 50,
        "B2": 100,
        "A3": "an old falcon",
        "A4": 3.14,
        "A5": true,
    }

    for cell, value := range values {
        if err := f.SetCellValue("Sheet1", cell, value); err != nil {
            log.Fatal(err)
        }
    }

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

    fmt.Println("saved")
}

The map only determines the order in which the cells are written, not their contents. The resulting file is a zip archive in the Office Open XML format, which file recognizes as an Excel workbook.

$ go run main.go
saved
$ file simple.xlsx
simple.xlsx: Microsoft Excel 2007+

Reading cells

OpenFile opens an existing workbook, and GetCellValue returns the formatted value of one cell as a string.

main.go
package main

import (
    "fmt"
    "log"

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

func main() {

    f, err := excelize.OpenFile("simple.xlsx")
    if err != nil {
        log.Fatal(err)
    }
    defer f.Close()

    for _, cell := range []string{"A1", "B2", "A3", "A4", "A5"} {
        value, err := f.GetCellValue("Sheet1", cell)
        if err != nil {
            log.Fatal(err)
        }
        fmt.Printf("%s = %q\n", cell, value)
    }
}

Numbers lose their original Go type when they are read back; the boolean is formatted as TRUE. Use typed getters or the cell metadata when the type matters.

$ go run main.go
A1 = "50"
B2 = "100"
A3 = "an old falcon"
A4 = "3.14"
A5 = "TRUE"

Writing and reading rows

SetSheetRow writes a whole row starting at a cell, and GetRows returns all non-empty rows of a sheet as a slice of string slices.

main.go
package main

import (
    "fmt"
    "log"

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

func main() {

    f := excelize.NewFile()
    defer f.Close()

    rows := [][]any{
        {"name", "age", "city"},
        {"Lucia", 32, "Bratislava"},
        {"Peter", 45, "Košice"},
        {"John", 28, "Prague"},
    }

    for i, row := range rows {
        cell, err := excelize.CoordinatesToCellName(1, i+1)
        if err != nil {
            log.Fatal(err)
        }
        if err := f.SetSheetRow("Sheet1", cell, &row); err != nil {
            log.Fatal(err)
        }
    }

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

    got, err := f.GetRows("Sheet1")
    if err != nil {
        log.Fatal(err)
    }

    for _, row := range got {
        fmt.Println(row)
    }

    fmt.Println("rows:", len(got))
}

CoordinatesToCellName converts a column number and a row number to a cell name such as A1. SetSheetRow expects a pointer to the slice, and UTF-8 text is preserved.

$ go run main.go
[name age city]
[Lucia 32 Bratislava]
[Peter 45 Košice]
[John 28 Prague]
rows: 4

Working with sheets

A workbook starts with one sheet. NewSheet adds another one and returns its index, GetSheetList lists the names and SetActiveSheet selects the sheet that is shown first.

main.go
package main

import (
    "fmt"
    "log"

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

func main() {

    f := excelize.NewFile()
    defer f.Close()

    f.SetCellValue("Sheet1", "A1", "first sheet")

    idx, err := f.NewSheet("Report")
    if err != nil {
        log.Fatal(err)
    }

    f.SetCellValue("Report", "A1", "report data")
    f.SetActiveSheet(idx)

    fmt.Println("index:", idx)
    fmt.Println("sheets:", f.GetSheetList())
    fmt.Println("active:", f.GetActiveSheetIndex())

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

The new sheet gets index 1 because the default sheet has index 0. Setting it active changes the sheet that a spreadsheet application opens first.

$ go run main.go
index: 1
sheets: [Sheet1 Report]
active: 1

Styling cells

A style combines a font, an alignment, a fill and a border. NewStyle returns a style index, and SetCellStyle applies it to a range. Column widths are set separately.

main.go
package main

import (
    "fmt"
    "log"

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

func main() {

    f := excelize.NewFile()
    defer f.Close()

    f.SetCellValue("Sheet1", "A1", "Quarterly report")
    f.SetColWidth("Sheet1", "A", "A", 24)

    style, err := f.NewStyle(&excelize.Style{
        Font:      &excelize.Font{Bold: true, Italic: true, Color: "C00000"},
        Alignment: &excelize.Alignment{Horizontal: "center", Vertical: "center"},
        Fill:      excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"FFF2CC"}},
    })
    if err != nil {
        log.Fatal(err)
    }

    if err := f.SetCellStyle("Sheet1", "A1", "A1", style); err != nil {
        log.Fatal(err)
    }

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

    cellStyle, err := f.GetCellStyle("Sheet1", "A1")
    if err != nil {
        log.Fatal(err)
    }

    width, err := f.GetColWidth("Sheet1", "A")
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println("style index:", style)
    fmt.Println("cell style: ", cellStyle)
    fmt.Println("column width:", width)
}

The color is written as an RGB hex string without the leading #. The style index returned when the style is created is the same one that the cell reports after the file is saved.

$ go run main.go
style index: 1
cell style:  1
column width: 24

Merging cells

MergeCell combines a rectangular range into one cell. The value of the top-left cell is kept, and GetMergeCells lists the merged ranges of a sheet.

main.go
package main

import (
    "fmt"
    "log"

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

func main() {

    f := excelize.NewFile()
    defer f.Close()

    f.SetCellValue("Sheet1", "A1", "Sunny Day")

    if err := f.MergeCell("Sheet1", "A1", "B2"); err != nil {
        log.Fatal(err)
    }

    merged, err := f.GetMergeCells("Sheet1")
    if err != nil {
        log.Fatal(err)
    }

    for _, m := range merged {
        fmt.Println(m.GetStartAxis(), m.GetEndAxis(), m.GetCellValue())
    }

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

    fmt.Println("saved")
}

The range is described by its top-left and bottom-right corners. Styling the merged range with SetCellStyle makes the text visible across the whole area.

$ go run main.go
A1 B2 Sunny Day
saved

Formulas

SetCellFormula stores a formula instead of a computed value, and CalcCellValue evaluates it without opening a spreadsheet application.

main.go
package main

import (
    "fmt"
    "log"

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

func main() {

    f := excelize.NewFile()
    defer f.Close()

    f.SetCellValue("Sheet1", "A1", 10)
    f.SetCellValue("Sheet1", "B1", 32)

    if err := f.SetCellFormula("Sheet1", "C1", "=A1+B1"); err != nil {
        log.Fatal(err)
    }

    formula, err := f.GetCellFormula("Sheet1", "C1")
    if err != nil {
        log.Fatal(err)
    }

    result, err := f.CalcCellValue("Sheet1", "C1")
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println("formula:", formula)
    fmt.Println("result: ", result)

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

The formula text starts with =. The calculation is performed by excelize, so the result is available even before the file is opened in Excel.

$ go run main.go
formula: =A1+B1
result:  42

Charts

AddChart places a chart on a sheet. The chart type and the data ranges are described with a Chart value; the title is a list of rich text runs.

main.go
package main

import (
    "fmt"
    "log"

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

func main() {

    f := excelize.NewFile()
    defer f.Close()

    countries := []string{"USA", "China", "UK", "Russia", "South Korea", "Germany"}
    medals := []int{46, 38, 29, 22, 13, 11}

    f.SetCellValue("Sheet1", "A1", "Country")
    f.SetCellValue("Sheet1", "B1", "Gold")

    for i := range countries {
        f.SetCellValue("Sheet1", fmt.Sprintf("A%d", i+2), countries[i])
        f.SetCellValue("Sheet1", fmt.Sprintf("B%d", i+2), medals[i])
    }

    chart := &excelize.Chart{
        Type: excelize.Col,
        Series: []excelize.ChartSeries{{
            Name:       "Sheet1!$B$1",
            Categories: "Sheet1!$A$2:$A$7",
            Values:     "Sheet1!$B$2:$B$7",
        }},
        Title: excelize.ChartTitle{
            Paragraph: []excelize.RichTextRun{{Text: "Olympic gold medals"}},
        },
    }

    if err := f.AddChart("Sheet1", "D2", chart); err != nil {
        log.Fatal(err)
    }

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

    fmt.Println("saved")
}

The series references a header cell for its name, the category range for the x axis and the value range for the bars. The chart is anchored at cell D2. The archive contains a drawing and a chart part.

$ go run main.go
saved
$ file chart.xlsx
chart.xlsx: Microsoft Excel 2007+
$ unzip -l chart.xlsx | grep -c 'xl/charts/'
1

Streaming large sheets

For large amounts of data, NewStreamWriter writes rows directly to the file instead of keeping the whole sheet in memory. Rows must be written in ascending order, and Flush ends the streaming session.

main.go
package main

import (
    "fmt"
    "log"

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

func main() {

    f := excelize.NewFile()
    defer f.Close()

    sw, err := f.NewStreamWriter("Sheet1")
    if err != nil {
        log.Fatal(err)
    }

    if err := sw.SetRow("A1", []any{"id", "value"}); err != nil {
        log.Fatal(err)
    }

    for i := 1; i <= 1000; i++ {
        cell, err := excelize.CoordinatesToCellName(1, i+1)
        if err != nil {
            log.Fatal(err)
        }
        if err := sw.SetRow(cell, []any{i, i * i}); err != nil {
            log.Fatal(err)
        }
    }

    if err := sw.Flush(); err != nil {
        log.Fatal(err)
    }

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

    rows, err := f.GetRows("Sheet1")
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println("rows:", len(rows))
    fmt.Println("last:", rows[len(rows)-1])
}

The writer creates a header and one thousand data rows. Reading the sheet back returns one thousand and one rows, and the last row contains the expected square.

$ go run main.go
rows: 1001
last: [1000 1000000]

Source

Go excelize - Github page
Go excelize - package documentation

In this article we have read and written Excel files with Go. We created a workbook, read cells and rows back, added sheets, applied styles, merged cells, stored and evaluated formulas, added a column chart and streamed a large sheet.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Go tutorials.