GitHub - nekrassov01/table: A high-performance table rendering library for Go

GitHub

10 min read Original article ↗

table logo

A high-performance table rendering library for Go, with streaming APIs for every output format.

CI Go Reference License

Table of contents

Overview

nekrassov01/table renders Go data as terminal tables, markup tables, or CSV records. Each output package provides Table for complete data sets and Stream for row-at-a-time output. Both preserve the selected format's structure and escaping rules.

See Runnable examples for a generated catalog of inputs, options, commands, and exact output.

  • table uses functional options for clear, reusable configuration.
  • In the bundled comparisons, table runs 6.3 to 6.7 times as fast as the next-fastest alternative; see Performance.
  • table reuses internal buffers to minimize steady-state allocations.
  • TableOf and StreamOf adapt typed slices and error-returning iterators.
  • text measures Unicode by terminal display width, including ambiguous character widths in CJK locales.
  • Format-specific options add headers, calculated footers, and placeholders. They also support transformations, alignment, decoration, and cell spans.

Motivation

The project was created for four reasons:

  • To provide a table renderer that is fast, efficient, and easy to use.
  • To provide row-at-a-time output across every format, which no comparable library offered at the time.
  • To manage table-oriented output formats for applications such as CLIs in one module.
  • To support the less common Backlog table notation, which only the author's earlier mintab project covered at the time.

Output formats

Choose an output package for the destination. The root table package provides shared interfaces, typed row adapters, and errors; it does not select an output format.

Package Output Use it for
text Unicode or ASCII bordered tables CLIs, terminals, and logs
html Semantic HTML tables Web pages and reports
markdown GFM tables READMEs and GitHub
backlog Backlog table notation Backlog issues and Wiki pages
csv CSV records with a configurable delimiter TSV, CSV, and data interchange

Text output gallery

The examples below show the text package in several configurations.

ASCII

ASCII table

Simple

Simple table

Compact layout with horizontal lines omitted and a colored rounded border style

Compact table

Row spans with a colored heavy border style

Table with row spans

Column spans with a colored light border style

Table with column spans

Stacked header with a colored light border style

Table with a stacked header

Calculated footer with CJK text and a colored light border style

Table with a calculated footer

Calculated footer with CJK text, value transformations, and a colored light border style

Table with transformed values

Complex values with a colored double border style

Table containing complex values

Installation

Install with:

go get github.com/nekrassov01/table

Quick start

TableOf and StreamOf adapt typed application data to rows of table.Value. Primitive cell constructors avoid interface boxing. Transformers also receive table.Value and can use typed accessors such as AsInt(). See Value inputs and migration for input types and transformer behavior.

Table

Use TableOf to keep application data typed until the row boundary. This example renders deployment status to a terminal.

package main

import (
    "log"
    "os"

    "github.com/nekrassov01/table"
    "github.com/nekrassov01/table/text"
)

type Deployment struct {
    Service string
    Desired int
    Ready   int
    Status  string
}

func deploymentRow(deployment Deployment) []table.Value {
    return []table.Value{
        table.String(deployment.Service),
        table.Int(deployment.Desired),
        table.Int(deployment.Ready),
        table.String(deployment.Status),
    }
}

func main() {
    deployments := []Deployment{
        {Service: "payments", Desired: 4, Ready: 4, Status: "healthy"},
        {Service: "search", Desired: 3, Ready: 2, Status: "degraded"},
        {Service: "worker", Desired: 8, Ready: 8, Status: "healthy"},
    }

    output := text.NewTable(os.Stdout,
        text.WithHeader([]string{"SERVICE", "DESIRED", "READY", "STATUS"}),
        text.WithAlign(text.ScopeBody, text.Columns(1, 2), text.AlignRight),
        text.WithCompact(),
    )
    if err := output.Render(table.TableOf(deployments, deploymentRow)); err != nil {
        log.Fatal(err)
    }
}

This program produces the following table:

┌──────────┬─────────┬───────┬──────────┐
│ SERVICE  │ DESIRED │ READY │  STATUS  │
╞══════════╪═════════╪═══════╪══════════╡
│ payments │       4 │     4 │ healthy  │
│ search   │       3 │     2 │ degraded │
│ worker   │       8 │     8 │ healthy  │
└──────────┴─────────┴───────┴──────────┘

Stream

Use StreamOf to adapt an iter.Seq2[T, error] to streaming table rows. Each successful value becomes one output row, and the first source error is forwarded. Call Stream.Close even after an earlier error so it can attempt deferred output and release its internal workspace.

package report

import (
    "io"
    "iter"
    "time"

    "github.com/nekrassov01/table"
    "github.com/nekrassov01/table/text"
)

type AuditEvent struct {
    Time     time.Time
    Actor    string
    Action   string
    Resource string
}

func WriteAuditEvents(w io.Writer, events iter.Seq2[AuditEvent, error]) (err error) {
    output := text.NewStream(w,
        text.WithHeader([]string{"TIME", "ACTOR", "ACTION", "RESOURCE"}),
        text.WithCompact(),
    )
    defer func() {
        if closeErr := output.Close(); err == nil {
            err = closeErr
        }
    }()

    rows := table.StreamOf(events, func(event AuditEvent) []table.Value {
        return []table.Value{
            table.String(event.Time.Format(time.RFC3339)),
            table.String(event.Actor),
            table.String(event.Action),
            table.String(event.Resource),
        }
    })
    for row, sourceErr := range rows {
        if sourceErr != nil {
            return sourceErr
        }
        if renderErr := output.Render(row); renderErr != nil {
            return renderErr
        }
    }
    return nil
}

Given an iterator of audit events, the function produces output like this:

┌───────────────────────────┬────────────┬───────────┬──────────────┐
│           TIME            │   ACTOR    │  ACTION   │   RESOURCE   │
╞═══════════════════════════╪════════════╪═══════════╪══════════════╡
│ 2026-08-21T09:00:00+09:00 │ deploy-bot │ reconcile │ payments-api │
│ 2026-08-21T09:03:00+09:00 │ alice      │ scale     │ worker       │
│ 2026-08-21T09:08:00+09:00 │ bob        │ rollback  │ search-api   │
└───────────────────────────┴────────────┴───────────┴──────────────┘

Runnable examples

The generated examples catalog pairs shared input data with the exact options, commands, and output for every supported scenario. The same definitions drive the catalog, runnable examples, and benchmarks.

Use target to select the output package and mode to select the API. Use data to select the scenario. This command runs the simple text Table example:

make example target=text mode=table data=simple

Omit data to run every scenario for the selected package and mode. With no data selected, omit mode to run both APIs, or run make example without arguments to run every available example.

Table library comparison

The following tables compare the public APIs in the versions pinned by the benchmark module.

Formats

This table records the output implementations documented by each library. ✓ means the library provides a dedicated output mode for the format, and - means it does not. table targets the GFM table extension; the other Markdown entries indicate generic Markdown table output.

Output format table go-pretty v6.8.3 tablewriter v1.1.5 simpletable v1.0.0
Text ✓ ✓ ✓ ✓
HTML ✓ ✓ ✓ -
Markdown ✓ ✓ ✓ ✓
Backlog notation ✓ - - -
CSV or TSV ✓ ✓ - -
SVG - - ✓ -

Features

This table records whether each library exposes a direct public API for a capability in at least one output implementation. It does not imply that every format can express the capability.

Feature table go-pretty v6.8.3 tablewriter v1.1.5 simpletable v1.0.0
Typed value input ✓ - - -
Caller-defined row adapter ✓ - ✓ -
Reflection-based struct input - - ✓ -
CSV input - - ✓ -
Per-column transformation ✓ ✓ ✓ -
Built-in sorting and filtering - ✓ - -
Column hiding - ✓ ✓ -
Header ✓ ✓ ✓ ✓
Footer ✓ ✓ ✓ ✓
Index column - ✓ - -
Vertical merge ✓ ✓ ✓ -
Horizontal merge ✓ ✓ ✓ ✓
Placeholder ✓ ✓ (HTML) - -
Width, wrapping, and truncation ✓ ✓ ✓ -
Automatic terminal fit ✓ - - -
Title or caption ✓ ✓ ✓ -
Streaming API ✓ (All) - ✓ -
Pagination - ✓ - -
Pluggable output implementation - - ✓ -

Typed value input means constructors such as table.String() and table.Int() preserve primitive types without interface boxing. table.Any() remains available for arbitrary values. See Value inputs and migration.

The compared versions use these input APIs:

  • go-pretty defines Row as []interface{}.
  • tablewriter accepts Append(...interface{}) and Bulk(interface{}).
  • simpletable accepts strings through Cell.Text. Callers must format numeric values before assigning them.

For table, the feature matrix has the following qualifications:

  • Column hiding is intentionally left to input adaptation, so TableOf and StreamOf can omit fields before rows reach the output package.
  • Footer callbacks derive values such as totals and averages from captured state.
  • Merge behavior depends on the selected output format and is documented in the Public API guide.

The go-pretty placeholder entry refers to its HTML EmptyColumn setting.

Performance

Note

table is designed to minimize allocations and reuses internal buffers via sync.Pool. In steady-state benchmarks, common workloads reach one allocation per render; cold runs require additional allocations to initialize pooled state.

Run make bench target=comparison benchtime=1x count=1 to reduce steady-state amortization and expose one-iteration setup costs. For explicit pool-drained measurements of table, run make bench target=cold.

Run the comparison on your machine with make bench target=comparison benchtime=10000x count=5 cpuprofile= memprofile=. The following output records all five samples at commit 49a56df on an Apple M2 with Go 1.27.1, with profiling disabled:

$ make bench target=comparison benchtime=10000x count=5 cpuprofile= memprofile=
go test -benchmem -count 5 -benchtime 10000x   . -bench '^BenchmarkComparison'
goos: darwin
goarch: arm64
pkg: benchmarks
cpu: Apple M2
BenchmarkComparisonTableSimple-8          	   10000	      3129 ns/op	     210 B/op	       1 allocs/op
BenchmarkComparisonTableSimple-8          	   10000	      2157 ns/op	     208 B/op	       1 allocs/op
BenchmarkComparisonTableSimple-8          	   10000	      1834 ns/op	     208 B/op	       1 allocs/op
BenchmarkComparisonTableSimple-8          	   10000	      1696 ns/op	     208 B/op	       1 allocs/op
BenchmarkComparisonTableSimple-8          	   10000	      1657 ns/op	     208 B/op	       1 allocs/op
BenchmarkComparisonGoPrettySimple-8       	   10000	     10320 ns/op	    8152 B/op	     110 allocs/op
BenchmarkComparisonGoPrettySimple-8       	   10000	     10697 ns/op	    8152 B/op	     110 allocs/op
BenchmarkComparisonGoPrettySimple-8       	   10000	     10836 ns/op	    8152 B/op	     110 allocs/op
BenchmarkComparisonGoPrettySimple-8       	   10000	     10369 ns/op	    8152 B/op	     110 allocs/op
BenchmarkComparisonGoPrettySimple-8       	   10000	     11590 ns/op	    8152 B/op	     110 allocs/op
BenchmarkComparisonTableWriterSimple-8    	   10000	     89390 ns/op	  486951 B/op	     973 allocs/op
BenchmarkComparisonTableWriterSimple-8    	   10000	     88622 ns/op	  486951 B/op	     973 allocs/op
BenchmarkComparisonTableWriterSimple-8    	   10000	     90418 ns/op	  486951 B/op	     973 allocs/op
BenchmarkComparisonTableWriterSimple-8    	   10000	     88937 ns/op	  486951 B/op	     973 allocs/op
BenchmarkComparisonTableWriterSimple-8    	   10000	     91323 ns/op	  486951 B/op	     973 allocs/op
BenchmarkComparisonSimpleTableSimple-8    	   10000	     23144 ns/op	   13082 B/op	     425 allocs/op
BenchmarkComparisonSimpleTableSimple-8    	   10000	     24135 ns/op	   13089 B/op	     425 allocs/op
BenchmarkComparisonSimpleTableSimple-8    	   10000	     23233 ns/op	   13075 B/op	     425 allocs/op
BenchmarkComparisonSimpleTableSimple-8    	   10000	     23404 ns/op	   13082 B/op	     425 allocs/op
BenchmarkComparisonSimpleTableSimple-8    	   10000	     22597 ns/op	   13071 B/op	     425 allocs/op
BenchmarkComparisonTableComplex-8         	   10000	      9278 ns/op	    1133 B/op	      35 allocs/op
BenchmarkComparisonTableComplex-8         	   10000	      9216 ns/op	    1133 B/op	      35 allocs/op
BenchmarkComparisonTableComplex-8         	   10000	      9175 ns/op	    1133 B/op	      35 allocs/op
BenchmarkComparisonTableComplex-8         	   10000	      9136 ns/op	    1133 B/op	      35 allocs/op
BenchmarkComparisonTableComplex-8         	   10000	      9209 ns/op	    1133 B/op	      35 allocs/op
BenchmarkComparisonGoPrettyComplex-8      	   10000	     63260 ns/op	   49250 B/op	     317 allocs/op
BenchmarkComparisonGoPrettyComplex-8      	   10000	     60747 ns/op	   49251 B/op	     317 allocs/op
BenchmarkComparisonGoPrettyComplex-8      	   10000	     60804 ns/op	   49251 B/op	     317 allocs/op
BenchmarkComparisonGoPrettyComplex-8      	   10000	     62962 ns/op	   49251 B/op	     317 allocs/op
BenchmarkComparisonGoPrettyComplex-8      	   10000	     60516 ns/op	   49249 B/op	     317 allocs/op
BenchmarkComparisonTableWriterComplex-8   	   10000	    301172 ns/op	  720130 B/op	    4749 allocs/op
BenchmarkComparisonTableWriterComplex-8   	   10000	    296691 ns/op	  720124 B/op	    4749 allocs/op
BenchmarkComparisonTableWriterComplex-8   	   10000	    295675 ns/op	  720126 B/op	    4749 allocs/op
BenchmarkComparisonTableWriterComplex-8   	   10000	    299385 ns/op	  720127 B/op	    4749 allocs/op
BenchmarkComparisonTableWriterComplex-8   	   10000	    297917 ns/op	  720126 B/op	    4749 allocs/op
PASS
ok  	benchmarks	25.122s

The table summarizes those five samples. Each cell shows allocs/op · ns/op · B/op; all values are medians.

Scenario table go-pretty tablewriter simpletable
Simple 1 · 1,834 · 208 110 · 10,697 · 8,152 973 · 89,390 · 486,951 425 · 23,233 · 13,082
Complex values 35 · 9,209 · 1,133 317 · 60,804 · 49,251 4,749 · 297,917 · 720,126 -

- indicates that a library cannot express the scenario with the benchmark input.

The comparison benchmark uses the shared Simple and Complex data sets. Static data is converted to each library's required row type before timing begins. Each timed iteration constructs a table, processes the rows, and writes the result to a reused buffer. Complex compares native value handling rather than equivalent rendered bytes.

The benchmark preserves each library's configuration model. table uses only functional options, while go-pretty accumulates settings through setters. tablewriter combines constructor options with methods. simpletable receives prebuilt cells through exposed table sections. These native construction paths remain inside each timed iteration. Only the settings needed to align table structure and preserve header text are applied. Border characters and value formatting retain each library's defaults.

Documentation

Use these references to select the API, understand its design, and work on the module itself.

Resource Contents
Go Reference Exact declarations and symbol documentation
Public API guide Options, output behavior, defaults, and format capabilities
Architecture Package structure, data flow, and state ownership
Design specification Design decisions, invariants, tradeoffs, and non-goals
Development guide Test, benchmark, coverage, and analysis commands
Performance baseline Benchmark procedure and performance acceptance criteria

Author

nekrassov01

License

MIT