Documentation
¶
Overview ¶
Package colprint provides high-performance, zero-allocation column formatting for tabular data output in Go.
Overview ¶
colprint allows you to define fields for your struct types, compile field specifications into optimized write programs, and format millions of rows with zero allocations in the hot path.
Key Features ¶
- Type-safe using Go generics
- Zero allocations in formatting hot path
- Single syscall per row with line buffering
- Support for collections and default field sets
- Custom formatters for complex types
- Suitable for streaming and batch processing
Basic Usage ¶
// 1. Define your type
type Person struct {
Name string
Age int
}
// 2. Create registry and register fields
reg := colprint.NewRegistry[Person]()
reg.Field("name", "Name", "Person's name").
Width(15).
String(func(p *Person) string { return p.Name }).
Register()
reg.Field("age", "Age", "Age in years").
Width(5).
Int(func(p *Person) int { return p.Age }).
Register()
// 3. Compile a field specification
prog, _ := colprint.Compile(reg, "name,age")
// 4. Format rows (zero allocations)
line := make([]byte, 0, 256)
tmp := make([]byte, 0, 64)
prog.WriteHeader(os.Stdout, &line)
for i := range people {
prog.WriteRow(os.Stdout, &people[i], &tmp, &line)
}
Collections ¶
Group related fields into named collections:
reg.DefineCollection("basic", "name,age", "name", "age", "email")
reg.SetDefaults("basic", "name,age")
// Use @collection syntax in specs
prog, _ := colprint.Compile(reg, "@basic,custom_field")
Performance ¶
The library is designed for maximum performance:
- All field resolution happens once during Compile()
- Formatting uses typed closures (no interface dispatch)
- Buffers are reused across rows (caller-provided)
- No reflection in hot path
Expected performance: 1M+ rows/sec for typical workloads.
Current Limitations ¶
This initial version supports left-alignment only. Future versions will add right-alignment support for numeric fields.
Example (Basic) ¶
Example_basic demonstrates simple field registration and formatting.
package main
import (
"os"
"github.com/example/colprint"
)
// Person is our example domain type.
type Person struct {
Name string
Age int
Height float64
Kids int
}
func main() {
// Create a registry for Person type
reg := colprint.NewRegistry[Person]()
// Register fields using fluent API
reg.Field("name", "Name", "Person's full name").
Width(12).
String(func(p *Person) string { return p.Name }).
Register()
reg.Field("age", "Age", "Age in years").
Width(4).
Int(func(p *Person) int { return p.Age }).
Register()
// Compile a program
prog, _ := colprint.Compile(reg, "name,age")
// Create reusable buffers
line := make([]byte, 0, 128)
tmp := make([]byte, 0, 32)
// Write header
prog.WriteHeader(os.Stdout, &line)
prog.WriteUnderline(os.Stdout, &line)
// Write rows
people := []Person{
{Name: "Alice", Age: 30},
{Name: "Bob", Age: 25},
}
for i := range people {
prog.WriteRow(os.Stdout, &people[i], &tmp, &line)
}
}
Output: Name Age ---- --- Alice 30 Bob 25
Example (Collections) ¶
Example_collections shows how to use field collections.
package main
import (
"os"
"github.com/example/colprint"
)
// Person is our example domain type.
type Person struct {
Name string
Age int
Height float64
Kids int
}
func main() {
reg := colprint.NewRegistry[Person]()
// Register fields
reg.Field("name", "Name", "Person's name").
Width(10).
String(func(p *Person) string { return p.Name }).
Register()
reg.Field("age", "Age", "Age in years").
Width(4).
Int(func(p *Person) int { return p.Age }).
Register()
reg.Field("kids", "Kids", "Number of children").
Width(5).
Int(func(p *Person) int { return p.Kids }).
Register()
reg.Field("height", "Height", "Height in cm").
Width(8).
Float(1, func(p *Person) float64 { return p.Height }).
Register()
// Define collections
reg.DefineCollection("basic", "name,age", "name", "age")
reg.DefineCollection("family", "kids", "kids")
// Use collections with @ prefix
prog, _ := colprint.Compile(reg, "@basic,@family")
line := make([]byte, 0, 128)
tmp := make([]byte, 0, 32)
prog.WriteHeader(os.Stdout, &line)
person := Person{Name: "Carol", Age: 45, Kids: 2}
prog.WriteRow(os.Stdout, &person, &tmp, &line)
}
Output: Name Age Kids Carol 45 2
Example (Csv) ¶
Example_csv demonstrates CSV-style output with custom separator.
package main
import (
"os"
"github.com/example/colprint"
)
// Person is our example domain type.
type Person struct {
Name string
Age int
Height float64
Kids int
}
func main() {
reg := colprint.NewRegistry[Person]()
// For CSV output, width determines max field size
reg.Field("name", "Name", "Person's name").
Width(5).
String(func(p *Person) string { return p.Name }).
Register()
reg.Field("age", "Age", "Age in years").
Width(3).
Int(func(p *Person) int { return p.Age }).
Register()
// For CSV output, use NoPadding option
opts := colprint.Options{
Separator: ",",
NoPadding: true,
}
prog, _ := colprint.CompileWithOptions(reg, "name,age", opts)
line := make([]byte, 0, 128)
tmp := make([]byte, 0, 32)
prog.WriteHeader(os.Stdout, &line)
people := []Person{
{Name: "Alice", Age: 30},
{Name: "Bob", Age: 25},
}
for i := range people {
prog.WriteRow(os.Stdout, &people[i], &tmp, &line)
}
}
Output: Name,Age Alice,30 Bob,25
Example (Custom) ¶
Example_custom demonstrates custom formatters for complex types.
package main
import (
"math"
"os"
"strconv"
"github.com/example/colprint"
)
// Person is our example domain type.
type Person struct {
Name string
Age int
Height float64
Kids int
}
func main() {
reg := colprint.NewRegistry[Person]()
reg.Field("name", "Name", "Person's name").
Width(10).
String(func(p *Person) string { return p.Name }).
Register()
// Custom formatter: convert cm to feet/inches
reg.Field("height", "Height", "Height in imperial units").
Width(10).
Custom(func(dst []byte, p *Person) []byte {
totalIn := p.Height / 2.54
feet := int(math.Floor(totalIn / 12))
inches := int(math.Round(totalIn)) % 12
dst = strconv.AppendInt(dst, int64(feet), 10)
dst = append(dst, '\'')
dst = strconv.AppendInt(dst, int64(inches), 10)
dst = append(dst, '"')
return dst
}).
Register()
prog, _ := colprint.Compile(reg, "name,height")
line := make([]byte, 0, 128)
tmp := make([]byte, 0, 32)
prog.WriteHeader(os.Stdout, &line)
person := Person{Name: "Bob", Height: 175.0}
prog.WriteRow(os.Stdout, &person, &tmp, &line)
}
Output: Name Height Bob 5'9"
Example (Help) ¶
Example_help demonstrates the help functionality.
package main
import (
"bytes"
"fmt"
"github.com/example/colprint"
)
// Person is our example domain type.
type Person struct {
Name string
Age int
Height float64
Kids int
}
func main() {
reg := colprint.NewRegistry[Person]()
reg.Field("name", "Name", "Person's full name").
Width(12).
Category("Basic").
String(func(p *Person) string { return p.Name }).
Register()
reg.Field("age", "Age", "Age in years").
Width(4).
Category("Basic").
Int(func(p *Person) int { return p.Age }).
Register()
reg.Field("height", "Height", "Height in centimeters").
Width(8).
Category("Physical").
Float(1, func(p *Person) float64 { return p.Height }).
Register()
// Print help
var buf bytes.Buffer
reg.PrintHelp(&buf, "")
// Show output
fmt.Print(buf.String())
}
Output: Basic: Field Display Description age Age Age in years name Name Person's full name Physical: Field Display Description height Height Height in centimeters
Example (Streaming) ¶
Example_streaming shows handling of streaming data.
package main
import (
"bytes"
"fmt"
"github.com/example/colprint"
)
// Person is our example domain type.
type Person struct {
Name string
Age int
Height float64
Kids int
}
func main() {
reg := colprint.NewRegistry[Person]()
reg.Field("name", "Name", "Person's name").
Width(10).
String(func(p *Person) string { return p.Name }).
Register()
reg.Field("age", "Age", "Age in years").
Width(4).
Int(func(p *Person) int { return p.Age }).
Register()
prog, _ := colprint.Compile(reg, "name,age")
// Simulate streaming data
stream := make(chan Person, 3)
go func() {
stream <- Person{Name: "Alice", Age: 30}
stream <- Person{Name: "Bob", Age: 25}
stream <- Person{Name: "Carol", Age: 35}
close(stream)
}()
// Print header once
var buf bytes.Buffer
line := make([]byte, 0, 128)
tmp := make([]byte, 0, 32)
prog.WriteHeader(&buf, &line)
// Process stream
for person := range stream {
prog.WriteRow(&buf, &person, &tmp, &line)
}
fmt.Print(buf.String())
}
Output: Name Age Alice 30 Bob 25 Carol 35
Example (WidthOverride) ¶
Example_widthOverride shows how to override field widths.
package main
import (
"os"
"github.com/example/colprint"
)
// Person is our example domain type.
type Person struct {
Name string
Age int
Height float64
Kids int
}
func main() {
reg := colprint.NewRegistry[Person]()
reg.Field("name", "Name", "Person's name").
Width(10).
String(func(p *Person) string { return p.Name }).
Register()
reg.Field("age", "Age", "Age in years").
Width(3).
Int(func(p *Person) int { return p.Age }).
Register()
// Override name width to 20
prog, _ := colprint.Compile(reg, "name:20,age")
line := make([]byte, 0, 128)
tmp := make([]byte, 0, 32)
prog.WriteHeader(os.Stdout, &line)
person := Person{Name: "Alexandria", Age: 33}
prog.WriteRow(os.Stdout, &person, &tmp, &line)
}
Output: Name Age Alexandria 33
Index ¶
- func InheritFieldsFrom[T any, S any](dest *Registry[T], source *Registry[S], mapper func(*T) *S)
- type Field
- type FieldBuilder
- func (b *FieldBuilder[T]) Custom(fn func(dst []byte, v *T) []byte) *FieldBuilder[T]
- func (b *FieldBuilder[T]) Float(precision int, fn func(*T) float64) *FieldBuilder[T]
- func (b *FieldBuilder[T]) Int(fn func(*T) int) *FieldBuilder[T]
- func (b *FieldBuilder[T]) Register()
- func (b *FieldBuilder[T]) String(fn func(*T) string) *FieldBuilder[T]
- func (b *FieldBuilder[T]) Width(w int) *FieldBuilder[T]
- type Kind
- type Options
- type Program
- func (p *Program[T]) FormatRow(v *T, tmp, line *[]byte) string
- func (p *Program[T]) HeaderString() string
- func (p *Program[T]) WriteHeader(w io.Writer, line *[]byte) error
- func (p *Program[T]) WriteRow(w io.Writer, v *T, tmp, line *[]byte) error
- func (p *Program[T]) WriteUnderline(w io.Writer, line *[]byte) error
- type Registry
- func (r *Registry[T]) AddRegistry(sub *Registry[T])
- func (r *Registry[T]) DefineCollection(name, defaultSpec string, fields ...string)
- func (r *Registry[T]) Field(name, display, description string) *FieldBuilder[T]
- func (r *Registry[T]) ListCollections() []string
- func (r *Registry[T]) ListFields(sorted bool) []string
- func (r *Registry[T]) PrintHelp(w io.Writer, collection string)
- func (r *Registry[T]) SetDefaults(collectionName, spec string)
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func InheritFieldsFrom ¶ added in v0.2.0
InheritFieldsFrom copies all fields from a source registry to the destination registry, transforming the field accessors using the provided mapper function.
This is useful when type T contains or embeds type S, and you want to reuse all field definitions from S's registry for T.
Example:
type TreeNode struct {
Proc // embedded
cutime float32
}
procReg := CreateProcRegistry()
treeReg := colprint.NewRegistry[TreeNode]()
colprint.InheritFieldsFrom(treeReg, procReg, func(n *TreeNode) *Proc { return &n.Proc })
// Now treeReg has all Proc fields, add TreeNode-specific fields
treeReg.Field("cutime", "CUtime", "Cumulative user time").Width(10)...
Types ¶
type Field ¶
type Field[T any] struct { // Name is the unique identifier for this field Name string // Display is the text shown in the column header Display string // Description provides help text for this field Description string // Width is the column width in characters Width int // Kind indicates the data type (String, Int, Float, Custom) Kind Kind // Precision specifies decimal places for Float fields Precision int // Value extractors - only one should be set based on Kind GetString func(*T) string GetInt func(*T) int GetFloat func(*T) float64 GetCustom func(dst []byte, v *T) []byte }
Field describes how to extract and format a field from type T.
Fields are created using the Registry.Field() builder pattern, not by constructing this struct directly.
type FieldBuilder ¶
type FieldBuilder[T any] struct { // contains filtered or unexported fields }
FieldBuilder provides a fluent API for field construction.
func (*FieldBuilder[T]) Custom ¶
func (b *FieldBuilder[T]) Custom(fn func(dst []byte, v *T) []byte) *FieldBuilder[T]
Custom configures this field with a custom formatter.
The formatter appends formatted bytes to dst and returns the result. This allows for complex formatting logic (e.g., timestamps, byte counts).
Example:
Custom(func(dst []byte, p *Person) []byte {
return append(dst, formatTimestamp(p.Created)...)
})
func (*FieldBuilder[T]) Float ¶
func (b *FieldBuilder[T]) Float(precision int, fn func(*T) float64) *FieldBuilder[T]
Float configures this field as a floating-point type.
The precision parameter specifies the number of decimal places (e.g., 2 for "3.14").
func (*FieldBuilder[T]) Int ¶
func (b *FieldBuilder[T]) Int(fn func(*T) int) *FieldBuilder[T]
Int configures this field as an integer type.
The provided function extracts the int value from the object.
func (*FieldBuilder[T]) Register ¶
func (b *FieldBuilder[T]) Register()
Register adds this field to the registry.
This is the final step in the builder chain.
func (*FieldBuilder[T]) String ¶
func (b *FieldBuilder[T]) String(fn func(*T) string) *FieldBuilder[T]
String configures this field as a string type.
The provided function extracts the string value from the object.
func (*FieldBuilder[T]) Width ¶
func (b *FieldBuilder[T]) Width(w int) *FieldBuilder[T]
Width sets the column width in characters.
type Options ¶
type Options struct {
// Separator is inserted between columns (default: " ")
Separator string
// NoPadding disables all column padding (useful for CSV)
NoPadding bool
// PadLastColumn pads the last column to its width (default: false)
// Setting to false avoids trailing spaces
PadLastColumn bool
// NoHeader skips header line generation
NoHeader bool
// NoUnderline skips underline generation
NoUnderline bool
}
Options configures program compilation.
type Program ¶
type Program[T any] struct { // contains filtered or unexported fields }
Program is a compiled, optimized formatting plan for type T.
Programs are created by Compile() and can be reused for formatting millions of rows with zero allocations.
func Compile ¶
Compile creates an optimized formatting program from a field specification.
The spec is a comma-separated list of field names, with optional features:
- Field width override: "name:20" sets width to 20
- Default expansion: "@default" expands to collection's default fields
- Collection expansion: "@collection_name" expands to collection fields
Examples:
Compile(reg, "name,age,email") Compile(reg, "name:20,age:5,email:30") Compile(reg, "@default,extra_field") Compile(reg, "@basic,@perf")
Returns an error if any field name is invalid or a collection doesn't exist.
func CompileWithOptions ¶
CompileWithOptions creates a program with custom options.
func (*Program[T]) FormatRow ¶
FormatRow formats a row and returns it as a string.
This is less efficient than WriteRow as it allocates a string. Prefer WriteRow for high-volume output.
func (*Program[T]) HeaderString ¶
HeaderString returns the header as a string.
func (*Program[T]) WriteHeader ¶
WriteHeader writes the column headers to w.
The line buffer is used for temporary storage and reused across calls. It should have adequate capacity (typically 256 bytes).
func (*Program[T]) WriteRow ¶
WriteRow formats and writes a single row to w.
This is the hot path - designed for zero allocations and maximum speed. Buffers tmp and line are reused across calls and should have adequate capacity (typically 64 and 256 bytes respectively).
The tmp buffer is used for formatting individual values. The line buffer accumulates the complete row before writing.
type Registry ¶
type Registry[T any] struct { // contains filtered or unexported fields }
Registry stores field definitions for type T.
Create a registry with NewRegistry, then use the Field() method to register fields via a fluent builder API.
Registries can be hierarchical - use AddRegistry to nest sub-registries with their own names for organized help output.
func NewRegistry ¶
NewRegistry creates a new unnamed field registry for type T.
func NewRegistryWithName ¶ added in v0.2.0
NewRegistryWithName creates a named field registry for type T.
Named registries are useful for organizing related fields into sections when building hierarchical registries with AddRegistry().
func (*Registry[T]) AddRegistry ¶ added in v0.2.0
AddRegistry adds a sub-registry to this registry.
This enables hierarchical organization of fields. Sub-registries with names will appear as separate sections in help output.
Example:
procFields := colprint.NewRegistryWithName[TreeNode]("Process Fields")
colprint.InheritFieldsFrom(procFields, procReg, mapper)
reg.AddRegistry(procFields)
func (*Registry[T]) DefineCollection ¶
DefineCollection creates a named collection of fields.
The defaultSpec is the comma-separated list of fields used when this collection is referenced with @collection_name. The fields list contains all fields that belong to this collection (for help display).
Example:
reg.DefineCollection("basic", "name,age", "name", "age", "email", "phone")
func (*Registry[T]) Field ¶
func (r *Registry[T]) Field(name, display, description string) *FieldBuilder[T]
Field starts building a new field definition.
Use the returned FieldBuilder to configure the field, then call Register() to add it to the registry.
Example:
reg.Field("age", "Age", "Age in years").
Width(5).
Int(func(p *Person) int { return p.Age }).
Register()
func (*Registry[T]) ListCollections ¶
ListCollections returns all collection names in alphabetical order.
func (*Registry[T]) ListFields ¶
ListFields returns all registered field names.
By default, fields are returned in insertion order. If sorted is true, they are returned in alphabetical order instead.
func (*Registry[T]) PrintHelp ¶
PrintHelp writes formatted help for all fields to w.
If collection is non-empty, only fields in that collection are shown. For hierarchical registries, sub-registries are shown as separate sections.
func (*Registry[T]) SetDefaults ¶
SetDefaults sets the default field specification for a collection.
When @default is used in a spec, it expands to this value.