When I want to programmatically generate excel tables, I usually use the tealeg/xlsx package, since it does a great job of reading and writing xlsx files, and is very easy to use. However, this time, I was working on a big and complex project, and I found myself writing a lot of repeating code. Here is a dummy example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
package main

import (
	"github.com/tealeg/xlsx"
)

func main() {
    f := xlsx.NewFile()

    sheet, err := f.AddSheet("Cars")
    if err != nil {
        panic(err)
    }

    row := sheet.AddRow()

    row.AddCell().SetString("Cars Data")

    row = sheet.AddRow()

    headings := []string{
        "Make",
        "Model",
        "Origin",
        "Year",
        "Cylinders",
        "Displacement",
        "Horsepower",
    }
    for _, v := range headings {
        row.AddCell().SetValue(v)
    }

    row = sheet.AddRow()

    row.AddCell().SetValue("Honda")
    row.AddCell().SetValue("Civic")
    row.AddCell().SetValue("Japan")
    row.AddCell().SetValue(1991)
    row.AddCell().SetValue(4)
    row.AddCell().SetValue(1.6)
    row.AddCell().SetValue(140)

    row = sheet.AddRow()

    row.AddCell().SetValue("Toyota")
    row.AddCell().SetValue("Century")
    row.AddCell().SetValue("Japan")
    row.AddCell().SetValue(1997)
    row.AddCell().SetValue(12)
    row.AddCell().SetValue(5.0)
    row.AddCell().SetValue(310)

    row = sheet.AddRow()

    row.AddCell().SetValue("Nissan")
    row.AddCell().SetValue("GTR")
    row.AddCell().SetValue("Japan")
    row.AddCell().SetValue(1998)
    row.AddCell().SetValue(6)
    row.AddCell().SetValue(2.6)
    row.AddCell().SetValue(276)

    if err := f.Save("cars.xlsx"); err != nil {
        panic(err)
    }
}

Hmmm…as we can see, we have to manually add each row, and then add cells one by one, which is not cool. An easy (and smart) way to make the code shorter and concise would be to implement Sheet, Row and Cell types that have methods that do the same things as in the code block above. This is exactly what the table package does, it’s simply a wrapper around the xlsx package. Here are the types in question, from higher level to lower level:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Table represents a table in a xlsx file.
type Table struct {
  file      *xlsx.File        // file is the xlsx file.
  sheets    []*Sheet          // sheets is the list of sheets in the file.
  sheetsMap map[string]*Sheet // sheetsMap is a map of sheets in the file.
}

// Sheet represents a sheet in a xlsx file.
type Sheet struct {
  name string // name of the sheet.
  rows []*Row // rows in the sheet.
}

// Row represents a row in a sheet.
type Row struct {
  cells  []*Cell // cells in the row.
  height float64 // height of the row.
}

// Cell represents a cell in a row.
type Cell struct {
  Content        any        // content of the cell.
  MergeH, MergeV int        // number of cells to merge horizontally and vertically, respectively.
  Style          *CellStyle // style of the cell.
}

// CellStyle is the style of the cell.
type CellStyle xlsx.Style

Now, here is an updated version of our main function from before. This time, we implement custom methods on the Sheet and Row structs.

Note: this implementation is different from the one in the table package.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package main

import "github.com/tealeg/xlsx"

type Sheet struct {
    xlsxSheet *xlsx.Sheet
}

func NewSheet(xlsxSheet *xlsx.Sheet) *Sheet {
    return &Sheet{xlsxSheet: xlsxSheet}
}

func (s *Sheet) AddRow() *Row {
    return &Row{row: s.xlsxSheet.AddRow()}
}

type Row struct {
    row *xlsx.Row
}

func (r *Row) AddCells(content ...any) {
    for _, c := range content {
        r.row.AddCell().SetValue(c)
    }
}

func main() {
    f := xlsx.NewFile()

    xlsxSheet, err := f.AddSheet("Cars")
    if err != nil {
        panic(err)
    }

    sheet := NewSheet(xlsxSheet)

    sheet.AddRow().AddCells("Cars Data")

    headings := []any{
        "Make",
        "Model",
        "Origin",
        "Year",
        "Cylinders",
        "Displacement",
        "Horsepower",
    }
    sheet.AddRow().AddCells(headings...)

    sheet.AddRow().AddCells("Honda", "Civic", "Japan", 1991, 4, 1.6, 158)
    sheet.AddRow().AddCells("Toyota", "Century", "Japan", 1997, 12, 5.0, 310)
    sheet.AddRow().AddCells("Nissan", "GTR", "Japan", 1998, 6, 2.6, 276)

    if err := f.Save("cars.xlsx"); err != nil {
        panic(err)
    }
}

And here is a picture of the table we just created:

table

See ? Instead of having to add each cell - and its contents - manually, we can just batch them together using the AddCells method. This is just one feature of the table package, other features include:

  • cell styling with chain syntax:
1
style := table.NewCellStyle().SetFontSize(14).SetBold(true).SetWrapText(true)
  • add borders to a group of rows:
1
2
3
4
5
// add thin inner borders.
table.AddOuterBorderRows("thin", rows...)
    
// add thick outer borders.
table.AddInnerBorderRows("thick", rows...)
  • generate common table components:
1
2
3
4
5
6
7
8
// generate a generic table that has a title, headings, and body.
func GenericTable(title string, headings []*Cell, rows []*Row, borderStyle string) []*Row

// generate a title row.
func RowTitle(title string, cols int, borderStyle string, cellStyle ...*CellStyle) *Row

// generate rows with cells that are merged horizontally and vertically.
func RowWithMergedCells(cells ...*Cell) []*Row

Yeah so that’s it I guess, thank you for reading.

  • table package source code.
  • xlsx package source code.