Skip to content
gohacks.dev
Testing

Compare Values in Go Tests With cmp Diff for Clear Failures

Compare structs, slices and maps in Go tests with cmp.Diff for a precise explanation of what differs, plus options for ignored fields, order, floats and time.

8 min read
Summarize in AI
Flat illustration of a test report with one failing test.

The most common assertion in a Go test is "this value equals that one", and the standard library gives you two weak ways to write it. == does not work on slices, maps or structs containing them. reflect.DeepEqual works on all of them and tells you only true or false. When it says false for a struct with twenty fields and nested slices, you are left to print both values and compare them by eye.

Tested with Go 1.25.6, go-cmp v0.7.0, macOS on Apple Silicon (arm64)

github.com/google/go-cmp fixes this with one function. cmp.Diff(want, got) returns an empty string when the values are equal and a diff when they are not, showing precisely which fields differ. Its second feature is a set of options for the cases where "equal" needs a looser meaning: ignore this field, treat these slices as unordered, accept floats within a tolerance. This article runs it on a realistic struct and goes through the options you will need. It builds on table-driven tests and golden files, and is the alternative to the assertion libraries compared in testify or the standard library. It ran on Go 1.25.6 with go-cmp v0.7.0.

The type

An order with the kinds of fields that make comparison awkward: a slice of structs, a map, a timestamp, a float and an unexported field.

order.goGo
package main

import "time"

type Item struct {
	SKU string
	Qty int
}

type Order struct {
	ID        string
	Customer  string
	Items     []Item
	Tags      map[string]string
	CreatedAt time.Time
	Total     float64
	note      string // unexported: cmp refuses to look inside unless told how
}
main.goGo
package main

func main() {}

What the diff looks like

The test builds an expected and an actual order, changes three things in the actual one (the customer name, one item's quantity, one tag), and prints both approaches.

cmp_test.goGo
package main

import (
	"fmt"
	"reflect"
	"strings"
	"testing"
	"time"

	"github.com/google/go-cmp/cmp"
	"github.com/google/go-cmp/cmp/cmpopts"
)

func sample() Order {
	return Order{ID: "o1", Customer: "Ada", Items: []Item{{"kb", 1}, {"mouse", 2}}, Tags: map[string]string{"channel": "web"},
		CreatedAt: time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC), Total: 29.97}
}

func TestShowWhatCmpPrints(t *testing.T) {
	want, got := sample(), sample()
	got.Items[1].Qty = 3
	got.Tags["channel"] = "app"
	got.Customer = "Ada L."

	fmt.Println("reflect.DeepEqual says equal:", reflect.DeepEqual(want, got), "(and nothing about why)")
	fmt.Println("cmp.Diff (-want +got):")
	fmt.Print(indent(cmp.Diff(want, got, cmpopts.IgnoreUnexported(Order{}))))
}

func TestUnexportedFields(t *testing.T) {
	defer func() {
		r := recover()
		msg := fmt.Sprint(r)
		fmt.Println("comparing a struct with an unexported field panics:", strings.HasPrefix(msg, "cannot handle unexported field"))
	}()
	cmp.Diff(sample(), sample())
}

func TestOptions(t *testing.T) {
	a, b := sample(), sample()
	b.ID = "generated-123"
	b.CreatedAt = a.CreatedAt.Add(300 * time.Millisecond)
	b.Items = []Item{a.Items[1], a.Items[0]} // same items, other order
	b.Total = a.Total + 0.0000001

	opts := cmp.Options{
		cmpopts.IgnoreUnexported(Order{}),
		cmpopts.IgnoreFields(Order{}, "ID"),                               // a generated id we cannot predict
		cmpopts.EquateApproxTime(time.Second),                             // timestamps within a second
		cmpopts.SortSlices(func(x, y Item) bool { return x.SKU < y.SKU }), // order does not matter
		cmpopts.EquateApprox(0, 1e-6),                                     // floating point noise
	}
	fmt.Println("with options, the difference is empty:", cmp.Diff(a, b, opts) == "")

	c := sample()
	c.Items = nil
	d := sample()
	d.Items = []Item{}
	fmt.Println("nil and empty slice differ without EquateEmpty:", cmp.Diff(c, d, cmpopts.IgnoreUnexported(Order{})) != "")
	fmt.Println("and match with it:", cmp.Diff(c, d, cmpopts.IgnoreUnexported(Order{}), cmpopts.EquateEmpty()) == "")

	fold := cmp.Comparer(func(x, y string) bool { return strings.EqualFold(x, y) })
	fmt.Println("custom comparer, case-insensitive strings equal:", cmp.Equal("Ada", "ADA", fold))
}

func indent(s string) string {
	return "    " + strings.ReplaceAll(strings.TrimRight(s, "\n"), "\n", "\n    ") + "\n"
}
Shell
go test -count=1 -v -run 'TestShowWhatCmpPrints|TestUnexportedFields|TestOptions' . 2>&1 | grep -vE '^(=== RUN|--- PASS|PASS|ok)'
Text
reflect.DeepEqual says equal: false (and nothing about why)
cmp.Diff (-want +got):
      main.Order{
      	ID:       "o1",
    - 	Customer: "Ada",
    + 	Customer: "Ada L.",
      	Items: []main.Item{
      		{SKU: "kb", Qty: 1},
      		{
      			SKU: "mouse",
    - 			Qty: 2,
    + 			Qty: 3,
      		},
      	},
    - 	Tags:      map[string]string{"channel": "web"},
    + 	Tags:      map[string]string{"channel": "app"},
      	CreatedAt: s"2026-01-02 03:04:05 +0000 UTC",
      	Total:     29.97,
      	... // 1 ignored field
      }
comparing a struct with an unexported field panics: true
with options, the difference is empty: true
nil and empty slice differ without EquateEmpty: true
and match with it: true
custom comparer, case-insensitive strings equal: true

reflect.DeepEqual returned false and nothing else. cmp.Diff printed this (shortened here):

Text
  main.Order{
  	ID:       "o1",
- 	Customer: "Ada",
+ 	Customer: "Ada L.",
  	Items: []main.Item{
  		{SKU: "kb", Qty: 1},
  		{
  			SKU: "mouse",
- 			Qty: 2,
+ 			Qty: 3,
  		},
  	},
- 	Tags:      map[string]string{"channel": "web"},
+ 	Tags:      map[string]string{"channel": "app"},
  	CreatedAt: s"2026-01-02 03:04:05 +0000 UTC",
  	Total:     29.97,
  	... // 1 ignored field
  }

Lines starting with - are from the first argument (want), lines with + from the second (got), and unprefixed lines are context. Only the three differences are marked, even though the struct has seven fields, and the unchanged items inside Items stay collapsed on one line. The line ... // 1 ignored field appears because the test passed an option to ignore the unexported field. This is the output you want in a failing test: you do not have to look anywhere else.

Use it in a test like this:

Go
if diff := cmp.Diff(want, got); diff != "" {
	t.Errorf("order mismatch (-want +got):\n%s", diff)
}

Putting (-want +got) in the message tells the reader how to interpret the signs. The order of the arguments matters only for that convention; be consistent.

Unexported fields

By default cmp refuses to compare unexported fields, because their equality is the type's business and not the caller's. The second test shows it: calling cmp.Diff on two Order values with the note field panics (the message starts cannot handle unexported field). That is intentional, and the panic is a test failure with an explanation.

You have three ways out. cmpopts.IgnoreUnexported(Order{}) ignores every unexported field of that type, as the lab does. cmp.AllowUnexported(Order{}) compares them as well. Or the type can define an Equal method (func (o Order) Equal(other Order) bool), which cmp uses automatically, so the type decides what equality means. For types you control and compare often, the Equal method is the cleanest.

Options for loose equality

Real code returns values with parts you cannot or should not compare exactly. The third test combines five options and shows that two orders which differ in several ways are equal under them:

  • cmpopts.IgnoreFields(Order{}, "ID") skips a field, here an ID generated at run time.
  • cmpopts.EquateApproxTime(time.Second) treats timestamps within a second as equal; the test shifted the time by 300 ms.
  • cmpopts.SortSlices(less) sorts slices before comparing, so order does not matter; the items were reversed.
  • cmpopts.EquateApprox(fraction, margin) compares floats within a margin; the totals differed by 1e-7, the margin was 1e-6.
  • cmpopts.IgnoreUnexported(Order{}) as before.

With all of them, cmp.Diff(a, b, opts) == "" was true. Without the options, each difference would be a failure. Keep the options in one place per type (a package-level cmp.Options variable in your test files) and reuse them, so a test states what it ignores and the ignoring is uniform.

Other useful options: cmpopts.IgnoreMapEntries and IgnoreSliceElements to drop elements by predicate, cmpopts.SortMaps for maps with unorderable keys, cmpopts.AcyclicTransformer to compare a transformed view, cmp.FilterPath to apply an option only at a given place in the value, and protocmp.Transform() (from the protobuf module) for protobuf messages, which cannot be compared with plain cmp (protobuf in Go).

Nil and empty

A frequent source of confusing failures is nil versus empty. A function that returns nil where the test expected []string{} fails a plain comparison, though callers would see no difference. The lab shows it: the nil and empty Items slices differed by default and matched with cmpopts.EquateEmpty(). Decide whether the distinction matters to your API. For JSON, it does (null versus [], see null versus omitted); for internal code, usually not.

Custom comparisons

For one-off rules, cmp.Comparer takes a function of two values of the same type: the lab's case-insensitive string comparer makes "Ada" equal "ADA". A comparer must be symmetric, deterministic and pure. For a type whose equality is more complex, give it an Equal method instead.

Habits that make cmp pleasant

Keep a want value that you build explicitly and a got value from the code; construct want from literals, never from the function under test. Name the options once per type in a test helper (var orderOpts = cmp.Options{...}) and reuse it, so a change to what counts as equal is one edit. Put the diff in the failure message with the (-want +got) hint. And when a diff is huge, narrow the comparison to the part you are testing, with a projection (compare just the item SKUs) or cmpopts.IgnoreFields, so the failure message stays short enough to read.

Why not reflect.DeepEqual, or an assertion library?

reflect.DeepEqual has semantics that surprise people: it treats nil and empty slices as different, compares unexported fields (which can hide that you are comparing implementation detail), follows pointers and fails on functions. It gives no explanation. Assertion libraries such as testify's assert.Equal print a diff, based on a similar idea, and have fewer ways to say what counts as equal. cmp is more verbose in its simplest form and much better at the hard cases, and it is the library the Go team uses in its own repositories. It has no assertions of its own; you keep if diff != "" and t.Errorf.

One caution: cmp.Diff is meant for tests. It uses reflection extensively and is not for production hot paths. The documentation is at pkg.go.dev/github.com/google/go-cmp/cmp, and the options package at cmpopts.

Also inBasics
Tagged""
Share

Keep reading

Get new articles by email

We send one email when a new article is published. No spam.

Unsubscribe in one click. No spam, ever.