As a Go developer, I’ve often faced the frustration of comparing complex data structures during testing or debugging sessions. Nested structs, maps with mixed types, or recursive types can turn a simple equality check into a headache. Go’s built-in reflect.DeepEqual
is a start, but when it fails, it leaves you guessing why two values differ. That’s where the dump
package steps in — a utility that transforms any Go value into a human-readable string, making differences easy to spot, especially with diff tools. After wrestling with these challenges myself, I created the dump
package to simplify the process, and I’m excited to share how it can help you.
The dump
package is part of the Ctx42 Testing Module, an evolving project aimed at building a flexible, developer-friendly testing framework for Go. In this article, I’ll walk you through its features, show you how to use it effectively, and explain why it’s a game-changer for testing and debugging in Go.
What is the dump Package?
The dump
package, available at github.com/ctx42/xtst/tree/master/pkg/dump, provides a configurable way to serialize any Go value — whether it’s a simple integer, a nested struct, or a recursive data structure — into a human-readable string. This is invaluable in testing, where comparing complex values often demands more than a boolean result. By rendering values as strings, the dump package lets you use string comparison or diff tools to quickly pinpoint discrepancies, offering clarity where reflect.DeepEqual
falls short.
Basic Usage
Getting started with the dump package is straightforward. Using its default configuration, you can dump a value with minimal setup. Here’s an example with a struct from the types package:
package main
import (
"fmt"
"time"
"github.com/ctx42/xtst/internal/types"
"github.com/ctx42/xtst/pkg/dump"
)
func main() {
val := types.TA{
Dur: 3,
Int: 42,
Loc: types.WAW,
Str: "abc",
Tim: time.Date(2000, 1, 2, 3, 4, 5, 0, time.UTC),
TAp: nil,
}
have := dump.DefaultDump().DumpAny(val)
fmt.Println(have)
}
Output:
{
Int: 42,
Str: "abc",
Tim: "2000-01-02T03:04:05Z",
Dur: "3ns",
Loc: "Europe/Warsaw",
TAp: nil,
}
The default configuration produces a nicely formatted, multi-line string. Fields are listed in the order they’re declared in the struct, ensuring consistent output for reliable comparisons.
Configuration Options
One of the dump
's strengths is its configurability. You can tweak how values are rendered to fit your needs. Here are the key options, each with practical examples:
Flat Output
For a compact, single-line representation, use the dump.Flat
option — ideal for logs or quick comparisons:
val := map[string]any{
"int": 42,
"loc": types.WAW,
"nil": nil,
}
cfg := dump.NewConfig(dump.Flat)
have := dump.New(cfg).DumpAny(val)
fmt.Println(have)
Output:
map[string]any{"int": 42, "loc": "Europe/Warsaw", "nil": nil}
When dumping maps, keys are sorted (when possible) to ensure consistent output.
Custom Time Formats
Customize how time.Time values appear with the dump.TimeFormat
option. This is great for aligning with your preferred timestamp style:
val := map[time.Time]int{
time.Date(2000, 1, 2, 3, 4, 5, 0, time.UTC): 42,
}
cfg := dump.NewConfig(dump.Flat, dump.TimeFormat(time.Kitchen))
have := dump.New(cfg).DumpAny(val)
fmt.Println(have)
Output:
map[time.Time]int{"3:04AM": 42}
Pointer Addresses
By default, pointer addresses are hidden, but you can reveal them with the dump.PtrAddr
option— useful for distinguishing between instances:
val := map[string]any{
"fn0": func() {},
"fn1": func() {},
}
cfg := dump.NewConfig(dump.PtrAddr)
have := dump.New(cfg).DumpAny(val)
fmt.Println(have)
Output (example addresses):
map[string]any{
"fn0": <func>(<0x533760>),
"fn1": <func>(<0x533780>),
}
Custom Dumpers
For ultimate flexibility, define custom dumpers to control how specific types are serialized. A custom dumper is a function matching the dump.Dumper
signature:
type Dumper func(dmp Dump, level int, val reflect.Value) string
Here’s an example that renders integers as hexadecimal values:
var i int
custom := func(dmp dump.Dump, lvl int, val reflect.Value) string {
switch val.Kind() {
case reflect.Int:
return fmt.Sprintf("%X", val.Int())
default:
panic("unexpected kind")
}
}
cfg := dump.NewConfig(dump.Flat, dump.Compact, dump.WithDumper(i, custom))
have := dump.New(cfg).DumpAny(42)
fmt.Println(have)
Output:
2A
This feature lets you tailor the output to your specific use case, enhancing readability or compatibility with other tools.
Handling Complex and Recursive Types
The dump package excels at managing complex and recursive data structures, with built-in cycle detection to prevent infinite loops. Here’s an example with a recursive struct:
type Node struct {
Value int
Children []*Node
}
val := &Node{
Value: 1,
Children: []*Node{
{Value: 2, Children: nil},
{Value: 3, Children: []*Node{{Value: 4, Children: nil}}},
},
}
have := dump.DefaultDump().DumpAny(val)
fmt.Println(have)
Output:
{
Value: 1,
Children: []*main.Node{
{
Value: 2,
Children: nil,
},
{
Value: 3,
Children: []*main.Node{
{
Value: 4,
Children: nil,
},
},
},
},
}
This structured output makes nested data easy to visualize and compare.
Using in Tests
The primary use case for the dump
package in testing is to visualize not equal values by dumping them as part of error message or as an input to a diff tool to highlight differences.
Extensibility
Built with extensibility in mind, the dump package lets you define custom dumpers for your own types. This adaptability ensures it can evolve with your project, integrating seamlessly with your specific needs.
Conclusion
Comparing complex data structures in Go doesn’t have to be a struggle. The dump package simplifies the process by turning any value into a human-readable string, ready for comparison or diffing. Its configurability — flat output, custom time formats, pointer addresses, and custom dumpers—makes it versatile, while its handling of recursive types and testing support make it indispensable.
I encourage you to try the dump package in your next project and explore the Ctx42 Testing Modules. It’s an evolving toolset, and your feedback or contributions could help shape its future. Let’s make testing in Go simpler, more reliable, and—yes—even enjoyable!
Top comments (0)