Pretty printing for complex Go types
go-pretty provides compact, single-line pretty printing of Go values suitable for logging and debugging. It handles circular references, implements smart truncation, and offers customizable formatting options.
- Single-line output: All values formatted on one line for easy log parsing
- Circular reference detection: Automatically detects and handles circular data structures
- Smart string handling: Byte slices containing valid UTF-8 are printed as strings
- Truncation support: Configurable limits for strings, errors, and slices
- Custom formatting: Implement
Printableinterface for custom types - Null handling: Support for nullable types via
Nullableinterface - Sorted maps: Map keys are automatically sorted for consistent output
- Context awareness: Special handling for
context.Contexttypes
go get github.com/domonda/go-prettyimport "github.com/domonda/go-pretty"
// Print to stdout
pretty.Println(myStruct)
pretty.Print(myValue)
// Print to string
s := pretty.Sprint(myValue)
// Print to io.Writer
pretty.Fprint(writer, myValue)
pretty.Fprintln(writer, myValue)All print functions accept optional indent arguments:
- No arguments: prints on a single line without indentation
- One argument: uses
indent[0]as indent string for nested structures - Two+ arguments: uses
indent[0]as indent string andindent[1:]concatenated as line prefix
// Single line output (no indentation)
pretty.Println(myStruct)
// Multi-line with 2-space indentation
pretty.Println(myStruct, " ")
// Multi-line with 2-space indentation and line prefix ">>> "
pretty.Println(myStruct, " ", ">>> ")
// Multi-line with line prefix concatenated from multiple strings
pretty.Println(myStruct, " ", "[", "LOG", "] ") // Line prefix: "[LOG] "printer := pretty.Printer{
MaxStringLength: 200, // Truncate strings longer than 200 chars
MaxErrorLength: 2000, // Truncate errors longer than 2000 chars
MaxSliceLength: 20, // Truncate slices with more than 20 elements
}
printer.Println(myValue)
s := printer.Sprint(myValue)// Print with fmt.Println as indented JSON
pretty.PrintlnAsJSON(myStruct)
// Custom indent print with fmt.Print
pretty.PrintAsJSON(myStruct, " ")Three interfaces are available for customizing how a type is pretty printed.
They are checked in order of priority: PrintableWithResult > Printable > Stringer.
Both value and pointer receivers are supported.
The simplest interface — write your representation to the writer:
type Color struct {
R, G, B uint8
}
func (c Color) PrettyPrint(w io.Writer) {
_, _ = fmt.Fprintf(w, "#%02x%02x%02x", c.R, c.G, c.B)
}pretty.Sprint(Color{255, 128, 0}) // #ff8000Like Printable but returns the number of bytes written and any error,
which allows the printer to accurately track output size:
type UserID int64
func (id UserID) PrettyPrint(w io.Writer) (n int, err error) {
return fmt.Fprintf(w, "user-%d", id)
}pretty.Sprint(UserID(42)) // user-42Return a string representation instead of writing to a writer:
type Status int
const (
StatusActive Status = 1
StatusDeleted Status = 2
)
func (s Status) PrettyString() string {
switch s {
case StatusActive:
return "active"
case StatusDeleted:
return "deleted"
default:
return fmt.Sprintf("Status(%d)", s)
}
}pretty.Sprint(StatusActive) // `active`Implement the Nullable interface to print "null" for zero values:
type MyNullable struct {
value *string
}
func (m MyNullable) IsNull() bool {
return m.value == nil
}The Printer.PrintFuncFor field allows you to customize how values are printed based on their reflect.Value. This is useful when you want to:
- Add custom formatting for types you don't control
- Adapt types that implement different interfaces (e.g.,
fmt.Stringer, custom serializers) - Change formatting based on runtime conditions
- Wrap values with additional context
You can use PrintFuncFor to enable pretty printing for types that implement other interfaces:
import (
"fmt"
"io"
"reflect"
"github.com/domonda/go-pretty"
)
// Assume you have types implementing fmt.Stringer or custom interfaces
type CustomStringer struct {
Name string
}
func (c CustomStringer) String() string {
return fmt.Sprintf("Custom<%s>", c.Name)
}
// Create a printer that handles fmt.Stringer types
printer := pretty.DefaultPrinter.WithPrintFuncFor(func(v reflect.Value) pretty.PrintFunc {
stringer, ok := v.Interface().(fmt.Stringer)
if !ok && v.CanAddr() {
stringer, ok = v.Addr().Interface().(fmt.Stringer)
}
if ok {
return func(w io.Writer) (int, error) {
return fmt.Fprint(w, stringer.String())
}
}
return pretty.PrintFuncForPrintable(v) // Use default
})
printer.Println(CustomStringer{Name: "test"})
// Output: Custom<test>// Mask sensitive data based on type or field tags
printer := pretty.DefaultPrinter.WithPrintFuncFor(func(v reflect.Value) pretty.PrintFunc {
// Customize based on type name
if v.Kind() == reflect.String && v.String() == "a sensitive string" {
return func(w io.Writer) (int, error) {
return fmt.Fprint(w, "`***REDACTED***`")
}
}
return pretty.PrintFuncForPrintable(v) // Use default
})
printer.Println("a sensitive string")
// Output: `***REDACTED***`Note: If Printer.PrintFuncFor is not set, the PrintFuncForPrintable function is used, which checks if the value implements the Printable interface.
The go-errs package uses a configurable Printer variable (of type *pretty.Printer) for formatting function parameters in error call stacks. You can customize this printer to mask secrets, adapt types, or change formatting without implementing the Printable interface on your types.
Use cases:
- Hide sensitive data (secrets, passwords, tokens) in error messages and stack traces
- Customize call stack formatting in error output
- Mask PII (Personally Identifiable Information) in logs
- Adapt types that implement other interfaces globally
import (
"fmt"
"io"
"reflect"
"strings"
"github.com/domonda/go-errs"
"github.com/domonda/go-pretty"
)
func init() {
// Configure the Printer used by go-errs for error call stacks
errs.Printer.PrintFuncFor = func(v reflect.Value) pretty.PrintFunc {
// Mask sensitive strings
if v.Kind() == reflect.String {
str := v.String()
// Check for common secret patterns
if strings.Contains(str, "password") ||
strings.Contains(str, "token") ||
strings.Contains(str, "secret") {
return func(w io.Writer) (int, error) {
return fmt.Fprint(w, "`***REDACTED***`")
}
}
}
// Hide sensitive struct fields
if v.Kind() == reflect.Struct {
t := v.Type()
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
// Check for "secret" tag
if field.Tag.Get("secret") == "true" {
// Return custom formatter that masks this field
// (implementation would format all fields except sensitive ones)
}
}
}
return pretty.PrintFuncForPrintable(v) // Use default
}
}
// Now all error stack traces from go-errs will automatically mask secrets
// without needing to implement Printable on your typesThis approach allows you to:
- Centrally control how all values are formatted in error call stacks
- Protect sensitive data in logs, error traces, and debug output
- Customize error formatting without modifying go-errs code
- Apply formatting rules to types you don't control
Note: The errs.Printer variable is a *pretty.Printer that can be fully configured with custom settings like MaxStringLength, MaxErrorLength, MaxSliceLength, and PrintFuncFor.
// Strings are backtick-quoted
pretty.Sprint("hello") // `hello`
// Structs show field names
type Person struct { Name string; Age int }
pretty.Sprint(Person{"Alice", 30}) // Person{Name:`Alice`;Age:30}
// Slices and arrays
pretty.Sprint([]int{1, 2, 3}) // [1,2,3]
// Maps with sorted keys
pretty.Sprint(map[string]int{"b": 2, "a": 1}) // map[string]int{`a`:1;`b`:2}
// Circular references
type Node struct { Next *Node }
n := &Node{}
n.Next = n
pretty.Sprint(n) // Node{Next:CIRCULAR_REF}
// Byte slices as strings
pretty.Sprint([]byte("hello")) // `hello`
// Time and Duration
pretty.Sprint(time.Now()) // Time(`2024-01-15T10:30:00Z`)
pretty.Sprint(5*time.Second) // Duration(`5s`)
// Nil values
pretty.Sprint((*int)(nil)) // nil
pretty.Sprint(error(nil)) // nil
// Context
ctx := context.Background()
pretty.Sprint(ctx) // Context{}The default printer used by package-level functions:
var DefaultPrinter = Printer{
MaxStringLength: 200,
MaxErrorLength: 2000,
MaxSliceLength: 20,
PrintFuncFor: nil,
}Set to 0 or negative values to disable truncation.
See LICENSE file