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/testing/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/testing/internal/types"
    "github.com/ctx42/testing/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.Default().Dump(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.WithFlat)
have := dump.New(cfg).Any(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.WithTimeFormat 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.WithFlat, dump.WithTimeFormat(time.Kitchen))
have := dump.New(cfg).Any(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.WithPtrAddr option — useful for distinguishing between instances:

val := map[string]any{
    "fn0": func() {},
    "fn1": func() {},
}

cfg := dump.NewConfig(dump.WithPtrAddr)
have := dump.New(cfg).Any(val)
fmt.Println(have)

Output (example addresses):

map[string]any{
    "fn0": (<0x533760>),
    "fn1": (<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.WithCompact, dump.WithDumper(i, custom))
have := dump.New(cfg).Any(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.Default().Any(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!