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.
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.
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.
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.
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.
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.
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.
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.
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.
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
List all Go tutorials.