DEV Community

Rafal Zajac
Rafal Zajac

Posted on

The dump Package For Go

#go

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)
}
Enter fullscreen mode Exit fullscreen mode

Output:

{
    Int: 42,
    Str: "abc",
    Tim: "2000-01-02T03:04:05Z",
    Dur: "3ns",
    Loc: "Europe/Warsaw",
    TAp: nil,
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Output:

map[string]any{"int": 42, "loc": "Europe/Warsaw", "nil": nil}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Output:

map[time.Time]int{"3:04AM": 42}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Output (example addresses):

map[string]any{
    "fn0": <func>(<0x533760>),
    "fn1": <func>(<0x533780>),
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Output:

2A
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Output:

{
    Value: 1,
    Children: []*main.Node{
        {
            Value: 2,
            Children: nil,
        },
        {
            Value: 3,
            Children: []*main.Node{
                {
                    Value: 4,
                    Children: nil,
                },
            },
        },
    },
}
Enter fullscreen mode Exit fullscreen mode

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)

🌶️ Newest Episode of Leet Heat: A Game Show For Developers!

Contestants face rapid-fire full stack web dev questions. Wrong answers? The spice level goes up. Can they keep cool while eating progressively hotter sauces?

View Episode Post

DEV is partnering to bring live events to the community. Join us or dismiss this billboard if you're not interested. ❤️