Open-source project
go-test/deep avatar
go-test/deep

deep.Equal turns a failed Go test into a list of differences

Golang deep variable equality test that returns human-readable differences

787 stars59 forksGoMIT

At a glance

What is it?
go-test/deep is a single-function Go package that compares variables and reports where they diverge, using the same reflect-based comparison as the standard library but printing a path for each mismatch. It suits teams that debug large structs, maps and slices in tests, and it carries a caveat: unexported fields are skipped.
Who is it for?
Adopt go-test/deep when a failing test only tells you that two large values differ and you need the path of the offending element. Skip it when the difference lives inside an unexported field, because the README states that deep.Equal does not compare unexported fields by default.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 10 days ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

One function that returns differences instead of a boolean

The whole public surface of this project is a single call, deep.Equal, and the README is explicit about the two reasons it exists next to reflect.DeepEqual. The first is the return shape: deep.Equal hands back a list of differences rather than a yes or no answer. The second is field selection: it does not compare unexported fields by default.

That first difference changes how a test reads. The standard library predicate gives you a boolean and leaves the searching to you, which the README describes as a game of Hunt the Wumpus, and the hunt gets expensive as maps, slices and structs grow. A list of differences hands you the location directly, so the failing assertion can print it instead of forcing the reader to bisect a large value by hand.

The comparison itself is still the familiar reflect-based one, so semantics stay close to what Go programmers already expect from DeepEqual. The contribution is diagnosis, not a different notion of equality.

Unexported fields sit outside the comparison

The README lists unexported fields as excluded by default, and the parenthetical is the important word. It implies there is a way to ask for them, yet the README never names the option, the function signature, or a configuration key that turns it on, and the example in the README compares only exported fields of a small struct.

For most test scenarios this is the behaviour you want. Comparing an unexported mutex or an unexported cache field would otherwise produce noise on every run, so skipping them keeps the diff focused on the state the test actually sets up. The cost is that two values that differ only below the package boundary can compare equal, and a test that means to assert on that difference will pass when it should fail.

If a project keeps its meaningful state in unexported fields, the package documentation on pkg.go.dev is the place to check for the opt-in, since the README itself does not describe it.

A first run: the example test and what it prints

The README gives one complete test, and it is small enough to copy into a scratch module. The two structs differ in a single float inside a slice, which is exactly the kind of mismatch that reflect.DeepEqual cannot localise for you. The test calls deep.Equal and reports the difference through t.Error.

go
package main_test

import (
	"testing"
	"github.com/go-test/deep"
)

type T struct {
	Name    string
	Numbers []float64
}

func TestDeepEqual(t *testing.T) {
	// Can you spot the difference?
	t1 := T{
		Name:    "Isabella",
		Numbers: []float64{1.13459, 2.29343, 3.010100010},
	}
	t2 := T{
		Name:    "Isabella",
		Numbers: []float64{1.13459, 2.29843, 3.010100010},
	}

	if diff := deep.Equal(t1, t2); diff != nil {
		t.Error(diff)
	}
}

Run the test and the failure message carries the path to the offending element:

code
$ go test
--- FAIL: TestDeepEqual (0.00s)
        main_test.go:25: [Numbers.slice[1]: 2.29343 != 2.29843]

The path names the struct field, the slice, and the index, then states both values. That string is the reason to reach for this package instead of the standard library comparison.

A library with no binary to run

The top-level listing of the repository is short: .github/, .gitignore, CHANGES.md, LICENSE, README.md, SECURITY.md, deep.go, deep_test.go, go.mod, go.sum, and a test/ directory. There is no command directory, so nothing here installs a CLI that you point at a file or a JSON payload. The package is consumed from your own test files.

That shape matters for how you evaluate the project. There is no configuration file to reconcile, no daemon to run, no port to expose, and no output format to parse. The only integration surface is the import path and the one function, which keeps the adoption cost inside a single test file.

It also means the diff rendering is not something you can extend from outside the package. The comparison logic and the formatting of each difference live in the same source file, and the repository offers no hook, plugin interface, or configuration mechanism for changing how a difference is described.

The module requires Go 1.24 and the release cadence is uneven

The module file is short and states two things that matter for installation:

code
module github.com/go-test/deep

go 1.24

The import path in the README example matches the module path exactly, so a project adds the dependency as github.com/go-test/deep. The go directive sets the floor at Go 1.24, so older toolchains cannot build the package.

Release history shows an uneven cadence rather than a steady one. v1.1.0 shipped on 2022-12-09, v1.1.1 on 2024-06-23, and v1.1.2 on 2026-10-13. That is roughly a year and a half between the first two tags and a little over two years before the third. The last push was on 2026-10-13, the same day as v1.1.2, so the current line of work is at v1.1.2. The repository is not archived.

Because the API surface is one function, an upgrade is unlikely to force large edits in your test files, but the CHANGES.md file in the repository is the place to confirm what a given tag actually changed before you bump the version.

Cases where deep.Equal is the wrong tool

Several situations argue against reaching for it. First, state that matters inside unexported fields is invisible to the comparison, so a test that mutates a private field can report no differences at all. Second, a difference list grows with the number of mismatches, and the README documents no limit on how many differences are collected, no ordering guarantee, and no way to compare values of different types.

Third, the comparison is exact. The README example demonstrates two float values that differ partway through the decimal fraction and reports them as unequal, which is correct and is also the only behaviour shown. Nothing in the README describes a tolerance option, a custom comparison rule, or special handling for types such as time.Time.

Fourth, the repository does not describe how the package treats recursive or self-referential structures. If your test data can contain a cycle, check the source file deep.go before relying on the result. In short, the package is a good fit for comparing plain data structures and a poor fit for comparing objects whose interesting state is private.

Where reflect.DeepEqual is still the better answer

The alternative already lives in the standard library, and the README compares against it directly. reflect.DeepEqual walks both values and reports a single boolean, comparing every field it can reach, including unexported ones, and it applies no special case for the types deep.Equal skips. deep.Equal walks the same structures but returns the path of each mismatch and leaves unexported fields alone by default.

That makes the choice about what you want to read in the failure output. If a test asserts on a private field, or if a single boolean from DeepEqual is enough because the values are small and the cause is obvious, the standard library call is simpler and adds no dependency. If a test compares a large map or a nested struct and the failure message needs to point at the exact element, the difference list is the reason to import this package.

Both calls are reflect-based, so neither changes how Go defines equality. The package does not promise a different equality model, only a friendlier report of the same comparison.

Editorial conclusion

Adopt go-test/deep when a failing test only tells you that two large values differ and you need the path of the offending element. Skip it when the difference lives inside an unexported field, because the README states that deep.Equal does not compare unexported fields by default. Before wiring it in, confirm your toolchain is at least Go 1.24, the version declared in go.mod, and check the CHANGES.md file for what changed in v1.1.2.

Frequently asked questions

What is go-test/deep and what does deep.Equal return?

It is a Go package that provides one function, deep.Equal. Instead of a boolean like reflect.DeepEqual, it returns a list of differences, such as Numbers.slice[1]: 2.29343 != 2.29843 in the README example.

How do I install go-test/deep and what Go version do I need?

The module is declared as github.com/go-test/deep in go.mod, and the README imports it under that same path. The go directive in go.mod requires Go 1.24 or newer, and the README gives no separate install command beyond adding the import.

Does deep.Equal compare unexported fields?

Not by default. The README lists as one of its two reasons for existing that deep.Equal does not compare unexported fields by default, and it does not document how to opt into comparing them.

Is go-test/deep actively maintained and how often are releases cut?

The repository is not archived and the last push was on 2026-10-13, the same day as the v1.1.2 release. Previous tags are spaced far apart: v1.1.1 on 2024-06-23 and v1.1.0 on 2022-12-09.

Does go-test/deep handle float values with a tolerance?

The README shows two float values that differ partway through the decimal fraction being reported as unequal, and it documents no tolerance option or custom comparison rule. Nothing indicates approximate comparison.

Official sources

  1. go-test/deep on GitHub
  2. Issues
  3. License: MIT
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/go-test-deep.svg)](https://hysenlabs.com/projects/go-test-deep)

Was this useful?