A small Go package for reading legacy Microsoft Excel .xls (BIFF8)
workbooks. It lists worksheet names and streams each sheet's rows as an
iterator without loading the whole sheet into memory.
- Open workbooks from a file path or any
io.ReaderAt - List the names of a workbook's visible worksheets
- Stream rows of a sheet as an
iter.Seq[Row]iterator as they are parsed, instead of buffering the whole sheet - String, numeric, date, boolean, and error cell values, including cached
results from formulas and strings stored as shared, inline, or rich-text
records, with spreadsheet-style column labels (
A,B, ...,AA, ...) - Explicitly blank cells recorded by Excel are returned as
TypeBlankcells - Each
RowreportsFirstCol/LastCol, the 0-based column range Excel recorded as having data, even if some of those columns are empty
go get github.com/b0r1ssh/go-xlspackage main
import (
"fmt"
"log"
"github.com/b0r1ssh/go-xls"
)
func main() {
f, err := xls.OpenFile("report.xls")
if err != nil {
log.Fatal(err)
}
for _, name := range f.SheetNames() {
fmt.Println("sheet:", name)
}
for row := range f.ReadRows(f.SheetNames()[0]) {
if row.Error != nil {
log.Fatal(row.Error)
}
for _, cell := range row.Cells {
fmt.Printf("%s%d = %v\n", cell.Column, row.Index+1, cell.Value)
}
}
}Row.Cells includes cells that Excel explicitly recorded as blank. Those
cells have TypeBlank and a nil value. Empty positions with no cell record
are omitted. Use Row.FirstCol and Row.LastCol to know the full column
range Excel recorded for the row instead of inferring it from Cells:
for row := range f.ReadRows("Sheet1") {
fmt.Printf("row %d spans columns %d to %d\n", row.Index, row.FirstCol, row.LastCol)
}Each Cell.Type reports the Go type stored in Cell.Value:
CellType |
Value type |
Example |
|---|---|---|
TypeString |
string |
"hello" |
TypeNumeric |
float64 |
3.14 |
TypeBoolean |
bool |
true |
TypeDate |
time.Time |
2024-01-01 |
TypeError |
string |
"#DIV/0!" |
TypeBlank |
nil |
Formula cells are returned with the result saved in the workbook; formulas are
not recalculated, and their expressions are not exposed. The saved result uses
the same cell types as other values: numeric results are TypeNumeric (or
TypeDate when the cell has a date format), and string, boolean, blank, and
error results use their corresponding types. Formula results cannot otherwise
be distinguished from literal cell values.
A cell holding a formula error (#DIV/0!, #N/A, #REF!, etc.) is reported
as TypeError rather than being silently dropped or folded into TypeString,
so callers can distinguish an error value from literal text that happens to
look like one:
for _, cell := range row.Cells {
if cell.Type == xls.TypeError {
log.Printf("%s%d has a formula error: %s", cell.Column, row.Index+1, cell.Value)
continue
}
// ...
}benchmarks/ compares reading a large .xls file with this
package and four other Go .xls readers, with results and instructions for
running it yourself in benchmarks/README.md.
Merging a pull request into main automatically creates a Git tag and a
GitHub release. Before merging, use at most one of these labels to select the
semantic version increment:
patch: bug fixes and other backward-compatible changes (v1.2.3tov1.2.4).minor: new backward-compatible functionality (v1.2.3tov1.3.0).major: breaking API changes (v1.2.3tov2.0.0).
When no release label is present, the workflow uses patch. Do not combine
patch, minor, and major labels on one pull request; the release workflow
will fail if more than one is present.
Use the skip-release label for documentation, CI, or other changes that do
not need a new module version. When a pull request with this label is merged,
the publish workflow skips the release entirely.