The spreadsheet requirement
Your team needs to export user analytics to a spreadsheet. The client insists on .xlsx format. They want bold headers, merged cells for the title, a formula that sums the total revenue, and a second sheet for raw data. You could generate a CSV file, but CSV lacks formatting, multiple sheets, and calculation logic. You need a library that speaks the Excel language.
Go does not include Excel support in the standard library. The ecosystem relies on third-party packages to handle the Open XML format. The most widely used library is excelize. It provides a high-level API to create, read, and modify Excel files without touching the underlying XML structure.
How Excel files actually work
An .xlsx file is not a binary blob. It is a ZIP archive containing a collection of XML files. The workbook structure, sheet data, styles, and relationships are all stored as text-based XML documents inside the archive. When you open an Excel file in a program, the application unzips the archive, parses the XML, and renders the grid.
excelize abstracts this complexity. You interact with Go types and methods. The library handles the XML generation, namespace management, and ZIP compression behind the scenes. You treat the file as a structured object in memory, and excelize serializes it to disk when you call save.
This abstraction has a cost. The library loads the entire workbook into memory. For small reports, this is invisible. For files with millions of rows, the memory footprint grows quickly. Understanding the memory model prevents runtime panics in production.
Creating a workbook from scratch
The simplest workflow starts with an empty workbook. You create the file instance, add sheets, write cell values, and save to disk. The library handles the default sheet creation and coordinate mapping.
Here is the minimal code to generate a spreadsheet with two sheets and cell data.
package main
import (
"fmt"
"log"
"github.com/xuri/excelize/v2"
)
func main() {
// NewFile creates an empty workbook in memory with a default Sheet1.
f := excelize.NewFile()
// NewSheet adds a new worksheet and returns its index.
// The index is needed to set the active sheet later.
index, err := f.NewSheet("Sheet2")
if err != nil {
log.Fatal(err)
}
// SetCellValue writes data to a cell using A1 notation.
// The library infers the type from the Go value.
f.SetCellValue("Sheet2", "A1", "Hello world.")
f.SetCellValue("Sheet2", "B1", 153)
// SetActiveSheet makes Sheet2 the default view when opened.
f.SetActiveSheet(index)
// SaveAs writes the workbook to disk.
// Always check the error to catch disk full or permission issues.
if err := f.SaveAs("Book1.xlsx"); err != nil {
log.Fatal(err)
}
}
The code creates a workbook, adds a sheet, writes a string and an integer, and saves the file. The SetCellValue method accepts any Go type. The library converts the Go value to the appropriate Excel data type. Strings become text. Integers and floats become numbers. Booleans become boolean values.
Convention aside: Go code should handle errors explicitly. The original snippet in many tutorials uses _ to discard the error from NewSheet. Discarding errors hides failures. If the sheet name is invalid or the file is corrupted, the program continues with a broken state. Use if err != nil to catch problems early. The underscore _ is for intentional discarding, such as ignoring a return value you truly do not need. Errors are rarely in that category.
Anatomy of a write operation
When you call SetCellValue, the library performs several steps. First, it validates the cell reference. Excel uses A1 notation, where columns are letters and rows are numbers. Column A is the first column. Row 1 is the first row. Cell A1 is the intersection.
The library converts the A1 string into internal row and column indices. It then locates the cell in the worksheet's data structure. If the cell does not exist, the library creates it. If the cell exists, the library updates the value.
Type conversion happens automatically. If you pass an integer 153, the library stores it as a number. If you pass the string "153", the library stores it as text. Excel treats these differently. Numbers can be used in formulas. Text cannot. The library respects the Go type you provide.
If you pass a type the library cannot handle, the compiler rejects the code. SetCellValue expects an any type in recent versions, but older versions or specific methods might be stricter. If you try to pass a complex struct without a stringer interface, the compiler complains with cannot use struct value as argument. The library supports basic types, slices, and pointers. Complex objects require manual serialization.
Memory allocation occurs for each cell written. The library keeps a map of cells in memory. Writing ten cells uses negligible memory. Writing one million cells allocates significant RAM. The workbook object holds references to all sheets, styles, and cell data until the file is saved or the object is garbage collected.
Excelize handles the XML. You handle the data.
Reading templates and applying styles
Real-world usage often involves reading an existing template, populating data, and applying formatting. You might have a header row with bold font, a specific background color, and merged cells. You load the template, iterate over your data, write rows, and save the result.
Styles in Excel are defined separately from cell values. You create a style object, configure font, fill, border, and alignment, and then apply the style to cells. This separation keeps the XML efficient. Multiple cells can reference the same style index.
Here is a realistic example that opens a template, writes data in a loop, applies a style to the header, and adds a formula.
package main
import (
"log"
"github.com/xuri/excelize/v2"
)
func generateReport() error {
// OpenFile loads an existing workbook from disk.
// Use OpenFile for templates. Use NewFile for blank workbooks.
f, err := excelize.OpenFile("template.xlsx")
if err != nil {
return err
}
// NewStyle creates a style object with configuration.
// Styles are added to the workbook's style table.
styleID, err := f.NewStyle(&excelize.Style{
Font: &excelize.Font{Bold: true, Color: "FFFFFF", Size: 12},
Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"4472C4"}},
Alignment: &excelize.Alignment{Horizontal: "center", Vertical: "center"},
})
if err != nil {
return err
}
// SetCellStyle applies the style to a range of cells.
// The range uses A1 notation: "A1:C1" covers three columns.
if err := f.SetCellStyle("Sheet1", "A1", "C1", styleID); err != nil {
return err
}
// Write data rows in a loop.
// Using a loop avoids repetitive code and keeps the function clean.
data := [][]interface{}{
{"Product A", 100, 20.50},
{"Product B", 50, 15.00},
{"Product C", 75, 12.75},
}
for i, row := range data {
// Row index starts at 2 because row 1 is the header.
rowNum := i + 2
f.SetCellValue("Sheet1", fmt.Sprintf("A%d", rowNum), row[0])
f.SetCellValue("Sheet1", fmt.Sprintf("B%d", rowNum), row[1])
f.SetCellValue("Sheet1", fmt.Sprintf("C%d", rowNum), row[2])
}
// SetCellFormula adds a calculation to a cell.
// Excel evaluates the formula when the file is opened.
f.SetCellFormula("Sheet1", "C5", "SUM(C2:C4)")
// SaveAs overwrites the file or creates a new one.
if err := f.SaveAs("report.xlsx"); err != nil {
return err
}
return nil
}
The code opens a template, defines a style with bold white text on a blue background, applies the style to the header row, writes three rows of data, and adds a sum formula. The NewStyle method returns a style ID. This ID links the cell to the style definition in the XML.
The loop uses fmt.Sprintf to generate cell references like A2, A3, A4. This is the standard way to iterate rows. The library also provides GetRows and GetCols for reading data, which return slices of strings.
Convention aside: Functions that perform I/O or heavy computation should accept a context.Context as the first parameter. If you are generating a large Excel file in a web handler, the request might time out. Passing context allows you to cancel the operation if the client disconnects. excelize does not natively support context, but you can check ctx.Err() inside your loop and break early.
Pitfalls and memory management
Excel files are heavy. The library loads the entire workbook into RAM. If you process a file with ten million rows, the process may consume gigabytes of memory. The garbage collector cannot reclaim memory while the workbook object is in use.
If you read a large file, modify a few cells, and save it, the library still loads everything. For massive datasets, consider streaming approaches. The standard library encoding/csv writes rows sequentially without loading the whole file. CSV lacks formatting, but it uses constant memory.
Type coercion can cause subtle bugs. Excel treats leading zeros as significant in text but drops them in numbers. If you write "00123" as a string, Excel displays 00123. If you write 123 as an integer, Excel displays 123. If you need to preserve leading zeros, such as for zip codes or product IDs, write the value as a string. The library respects the Go type.
Sheet names have restrictions. Excel limits sheet names to 31 characters. Names cannot contain colons, backslashes, asterisks, question marks, square brackets, or forward slashes. If you use a user-provided string as a sheet name, validate it first. The library returns an error if the name is invalid.
Compiler errors catch basic mistakes. If you forget to import the package, the compiler rejects the program with undefined: excelize. If you use a variable without declaring it, you get undefined: variable. If you import a package and do not use it, the compiler complains with imported and not used. Go enforces clean code. Fix the errors before running.
Runtime panics happen when you access a nil pointer or an out-of-bounds slice. The library returns errors for most failures. Check every error. Ignoring errors leads to silent data corruption. The worst bug is the one that writes garbage to a file without logging.
Memory is finite. Close files promptly.
Choosing the right tool
Go offers multiple ways to handle tabular data. The right choice depends on your requirements for formatting, performance, and memory.
Use excelize when you need full .xlsx support with multiple sheets, formulas, styles, and charts. Use it when the client demands a real Excel file and the data volume fits in memory.
Use encoding/csv when you only need raw data export and performance matters more than formatting. CSV files are small, fast to generate, and use constant memory. Use this for large datasets where formatting is irrelevant.
Use a pre-built template with excelize when the layout is static and complex. Loading a template saves you from writing code to define styles, merges, and headers. It also ensures consistency across reports.
Use a database export or streaming API when the data volume exceeds available memory. Generate the file in chunks or let the database handle the export. Do not load millions of rows into a Go struct.
Excelize is powerful. CSV is efficient. Pick the tool that matches the constraint.