Documentation
¶
Overview ¶
Package comparator provides advanced comparison and diffing capabilities for Go data structures.
This package offers a comprehensive solution for deep equality checking and difference detection across any Go values, including primitives, structs, slices, maps, pointers, and complex nested structures. It supports cycle detection, custom comparators, and multiple output formats.
Key Features ¶
- Deep recursive comparison with cycle detection
- Configurable comparison behavior (float precision, slice ordering, field ignoring)
- Multiple output formats: text, JSON, Markdown, HTML
- Unified diff format (Unix diff-style)
- JSON Patch generation (RFC 6902)
- Visual tree diff representation
- Custom comparators for specific types
- Detailed statistics and suggestions
Basic Usage ¶
Simple equality check:
comp := comparator.New()
if comp.Equal(obj1, obj2) {
fmt.Println("Objects are equal")
}
Or use the convenience function:
if comparator.Equal(obj1, obj2) {
fmt.Println("Objects are equal")
}
Comparison with Options ¶
Configure comparison behavior using functional options:
comp := comparator.NewWithOptions(
comparator.WithFloatPrecision(1e-6),
comparator.IgnoreSliceOrder(),
comparator.IgnoreStructFields("ID", "CreatedAt", "UpdatedAt"),
)
if comp.Equal(user1, user2) {
fmt.Println("Users are equal (ignoring ID and timestamps)")
}
Detailed Difference Analysis ¶
Get comprehensive diff information:
diffComp := comparator.NewDiffComparer(
comparator.WithOutputFormat("markdown"),
comparator.WithMaxDiffs(100),
)
result := diffComp.CompareWithDiff(expected, actual)
if !result.Equal {
fmt.Printf("Found %d differences\n", len(result.Differences))
fmt.Println(result.Summary)
for _, diff := range result.Differences {
fmt.Printf("[%s] %s: %s\n", diff.Severity, diff.Path, diff.Message)
for _, suggestion := range diff.Suggestions {
fmt.Printf(" → %s\n", suggestion)
}
}
}
JSON Patch Generation ¶
Generate RFC 6902 JSON Patch documents:
patch, err := comparator.GetJSONPatch(oldDoc, newDoc)
if err != nil {
log.Fatal(err)
}
patchJSON, _ := json.MarshalIndent(patch, "", " ")
fmt.Println(string(patchJSON))
// Output:
// [
// {"op": "replace", "path": "/name", "value": "John"},
// {"op": "add", "path": "/email", "value": "john@example.com"}
// ]
Unified Diff Format ¶
Generate Unix diff-style output:
diffComp := comparator.NewDiffComparer()
unifiedDiff, err := diffComp.GetUnifiedDiff(oldConfig, newConfig)
if err != nil {
log.Fatal(err)
}
fmt.Println(unifiedDiff.Header)
for _, chunk := range unifiedDiff.Chunks {
fmt.Println(chunk.Context)
for _, change := range chunk.Changes {
symbol := " "
if change.Type == "add" {
symbol = "+"
} else if change.Type == "remove" {
symbol = "-"
}
fmt.Printf("%s%s\n", symbol, change.Content)
}
}
Custom Comparators ¶
Register custom comparison logic for specific types:
type User struct {
ID int
Name string
Age int
}
comp := comparator.NewWithOptions(
comparator.WithCustomComparator(func(a, b User) bool {
return a.ID == b.ID // Compare users by ID only
}),
)
if comp.Equal(user1, user2) {
fmt.Println("Users have the same ID")
}
Configuration Options ¶
Available options for customizing comparison behavior:
- WithFloatPrecision(float64): Set precision for float comparisons
- IgnoreSliceOrder(): Treat slices as sets (ignore element order)
- WithMaxDepth(int): Limit recursion depth
- IgnoreUnexported(): Skip unexported struct fields
- EquateEmpty(): Treat nil and empty values as equal
- EquateNaNs(): Treat all NaN values as equal
- IgnoreStructFields(...string): Skip specific struct fields by name
- WithTimeLayout(string): Set time format for display
- WithCustomComparator[T](func(T, T) bool): Register custom comparator
- WithDiffMode(DiffMode): Set diff reporting mode
- WithMaxDiffs(int): Limit number of differences collected
- WithOutputFormat(string): Set output format (text/json/markdown/html)
- WithColorize(bool): Enable ANSI color codes in text output
- WithIncludeEqual(bool): Include equal values in diff reports
Colorized Output ¶
Enable ANSI color-coded terminal output for better readability:
comp := comparator.NewDiffComparer(
comparator.WithColorize(true),
comparator.WithOutputFormat("text"),
)
result := comp.CompareWithDiff(config1, config2)
formatted, _ := comp.FormatDiff(result, "text")
fmt.Println(formatted) // Displays with colors in terminal
The colorized output uses ANSI color codes:
- Cyan: Paths and section headers
- Green: Expected values and equal fields
- Red: Actual values and errors
- Yellow: Warnings
- Bold: Section headers
Use cases for colorized output:
- Terminal-based tools and CLI applications
- Development and debugging sessions
- Interactive diff viewers
- CI/CD pipeline outputs
Include Equal Values ¶
Include both differences and matches in comparison reports:
comp := comparator.NewDiffComparer(
comparator.WithIncludeEqual(true),
)
result := comp.CompareWithDiff(user1, user2)
for _, diff := range result.Differences {
if diff.Detail.Type == "equal" {
fmt.Printf("✓ %s: values match\n", diff.Path)
} else {
fmt.Printf("✗ %s: %s\n", diff.Path, diff.Message)
}
}
Equal values are marked with:
- Detail.Type = "equal"
- Severity = "info"
Use cases for including equal values:
- Comprehensive audit trails
- Configuration validation reports
- Debugging comparison logic
- Understanding what hasn't changed between versions
Combined Features ¶
Combine colorization and equal values for complete reports:
comp := comparator.NewDiffComparer(
comparator.WithColorize(true),
comparator.WithIncludeEqual(true),
comparator.WithOutputFormat("text"),
)
result := comp.CompareWithDiff(oldConfig, newConfig)
formatted, _ := comp.FormatDiff(result, "text")
fmt.Println(formatted)
This produces a color-coded report showing both differences and matches, making it easy to see the complete picture of the comparison.
Supported Types ¶
The comparator handles all Go types including:
- Primitives: bool, int*, uint*, float*, complex*, string
- Composite: arrays, slices, maps, structs
- Reference: pointers, interfaces, channels, functions
- Special: time.Time (uses native Equal method)
Thread Safety ¶
Comparator instances are not thread-safe. Create separate instances for concurrent use, or synchronize access with a mutex.
Performance Considerations ¶
- For repeated comparisons with the same configuration, reuse comparator instances
- Use WithMaxDepth to limit recursion for deeply nested structures
- Use WithMaxDiffs to limit memory usage when many differences exist
- Use IgnoreStructFields to skip expensive field comparisons
- For large slices, avoid IgnoreSliceOrder when possible (use sorted slices)
Error Handling ¶
Most methods return errors for future compatibility, but current implementations rarely return errors. Always check error returns for forward compatibility.
Example (ColorizedOutput) ¶
Example_colorizedOutput demonstrates using colored terminal output for better readability of differences.
type APIConfig struct {
Endpoint string
Timeout int
Retries int
}
config1 := APIConfig{
Endpoint: "https://api.example.com",
Timeout: 30,
Retries: 3,
}
config2 := APIConfig{
Endpoint: "https://api.example.com",
Timeout: 60,
Retries: 5,
}
// Create a comparator with colorized output enabled
comp := NewDiffComparer(
WithColorize(true),
WithOutputFormat("text"),
)
result := comp.CompareWithDiff(config1, config2)
formatted, err := comp.FormatDiff(result, "text")
if err != nil {
fmt.Printf("Error formatting diff: %v\n", err)
return
}
// The output will contain ANSI color codes for:
// - Cyan: paths and labels
// - Red: removed/expected values
// - Green: added/actual values
// - Yellow: warnings
fmt.Println(formatted)
// Note: In a terminal, this would display with colors.
// Without colorize, the output would be plain text.
Example (ColorizedWithEqualValues) ¶
Example_colorizedWithEqualValues demonstrates combining both features for a complete, colorized comparison report.
type DatabaseConfig struct {
Host string
Port int
Database string
SSL bool
}
oldConfig := DatabaseConfig{
Host: "localhost",
Port: 5432,
Database: "myapp",
SSL: false,
}
newConfig := DatabaseConfig{
Host: "localhost",
Port: 5432,
Database: "myapp_prod", // Changed
SSL: true, // Changed
}
// Combine colorization and including equal values
comp := NewDiffComparer(
WithColorize(true),
WithIncludeEqual(true),
WithOutputFormat("text"),
)
result := comp.CompareWithDiff(oldConfig, newConfig)
formatted, err := comp.FormatDiff(result, "text")
if err != nil {
fmt.Printf("Error formatting diff: %v\n", err)
return
}
// Output will show:
// - Equal fields (Host, Port) in green with "info" severity
// - Different fields (Database, SSL) in red with "error" severity
// - All paths highlighted in cyan
fmt.Println(formatted)
Example (DetailedDiff) ¶
// Complex JSON-like structures
// Original config
config1 := exampleConfig{
Version: "1.0.0",
Server: exampleServerConfig{
Host: "localhost",
Port: 8080,
SSL: false,
},
Database: exampleDatabaseConfig{
Host: "db.local",
Port: 5432,
Name: "appdb",
Username: "admin",
Password: "secret",
},
Features: map[string]bool{
"auth": true,
"logging": true,
"debug": false,
"monitoring": true,
},
Routes: []exampleRoute{
{
Path: "/api/users",
Methods: []string{"GET", "POST"},
Handler: "usersHandler",
Auth: true,
},
{
Path: "/api/products",
Methods: []string{"GET"},
Handler: "productsHandler",
Auth: false,
},
},
Cache: exampleCacheConfig{
Type: "redis",
TTL: 30 * time.Minute,
MaxSize: 1024 * 1024 * 100, // 100MB
Enabled: true,
},
}
// Modified config
config2 := exampleConfig{
Version: "1.0.1", // Changed
Server: exampleServerConfig{
Host: "localhost",
Port: 8080,
SSL: true, // Changed
},
Database: exampleDatabaseConfig{
Host: "db.local",
Port: 5432,
Name: "appdb",
Username: "admin",
Password: "newsecret", // Changed
},
Features: map[string]bool{
"auth": true,
"logging": true,
"debug": true, // Changed
"monitoring": true,
"caching": true, // Added
},
Routes: []exampleRoute{
{
Path: "/api/users",
Methods: []string{"GET", "POST", "PUT"}, // Changed
Handler: "usersHandler",
Auth: true,
},
{
Path: "/api/products",
Methods: []string{"GET"},
Handler: "productsHandler",
Auth: true, // Changed
},
{
Path: "/api/orders", // Added
Methods: []string{"GET", "POST"},
Handler: "ordersHandler",
Auth: true,
},
},
Cache: exampleCacheConfig{
Type: "redis",
TTL: 30 * time.Minute,
MaxSize: 1024 * 1024 * 500, // Changed
Enabled: true,
},
}
// Get detailed diff
result := CompareWithDiff(config1, config2,
IgnoreStructFields("Password"), // Ignore password changes
IgnoreSliceOrder(), // Ignore slice order in routes
WithDiffMode(DiffModeFull),
WithMaxDepth(10),
)
// Print formatted diff
formatted, err := NewDiffComparer().FormatDiff(result, "text")
if err != nil {
fmt.Printf("Error formatting diff: %v\n", err)
return
}
fmt.Println(formatted)
// Or get JSON Patch
patch, err := GetJSONPatch(config1, config2)
if err != nil {
fmt.Printf("Error generating patch: %v\n", err)
return
}
fmt.Printf("\nJSON Patch operations needed: %d\n", len(patch))
// Or get unified diff
unified, err := NewDiffComparer().GetUnifiedDiff(config1, config2)
if err != nil {
fmt.Printf("Error generating unified diff: %v\n", err)
return
}
fmt.Printf("\nUnified diff has %d chunks\n", len(unified.Chunks))
Example (IncludeEqualValues) ¶
Example_includeEqualValues demonstrates including both equal and different values in the comparison report.
type User struct {
ID int
Username string
Email string
Active bool
}
user1 := User{
ID: 123,
Username: "john_doe",
Email: "john@example.com",
Active: true,
}
user2 := User{
ID: 123,
Username: "john_doe",
Email: "john.doe@example.com", // Changed
Active: true,
}
// Create comparator that includes equal values in the report
comp := NewDiffComparer(
WithIncludeEqual(true),
WithOutputFormat("text"),
)
result := comp.CompareWithDiff(user1, user2)
fmt.Printf("Total differences (including equal): %d\\n", len(result.Differences))
// Count equal vs different
equalCount := 0
diffCount := 0
for _, diff := range result.Differences {
if diff.Detail.Type == "equal" {
equalCount++
} else {
diffCount++
}
}
fmt.Printf("Equal fields: %d\\n", equalCount)
fmt.Printf("Different fields: %d\\n", diffCount)
// This is useful for:
// - Comprehensive audit trails
// - Understanding what hasn't changed
// - Debugging comparison logic
Example (JsonDiff) ¶
// Compare JSON strings directly
json1 := `{
"user": {
"id": 12345,
"name": "John Doe",
"email": "john@example.com",
"preferences": {
"theme": "dark",
"language": "en"
},
"roles": ["admin", "user"]
}
}`
json2 := `{
"user": {
"id": 12345,
"name": "Jane Doe",
"email": "jane@example.com",
"preferences": {
"theme": "light",
"language": "en"
},
"roles": ["user", "admin"],
"active": true
}
}`
var data1, data2 any
if err := json.Unmarshal([]byte(json1), &data1); err != nil {
fmt.Printf("Error unmarshaling json1: %v\n", err)
return
}
if err := json.Unmarshal([]byte(json2), &data2); err != nil {
fmt.Printf("Error unmarshaling json2: %v\n", err)
return
}
result := CompareWithDiff(data1, data2,
IgnoreSliceOrder(),
WithDiffMode(DiffModeFull),
)
// Format as Markdown
md, err := NewDiffComparer().FormatDiff(result, "markdown")
if err != nil {
fmt.Printf("Error formatting as markdown: %v\n", err)
return
}
fmt.Println(md)
// Or generate HTML report
html, err := NewDiffComparer().FormatDiff(result, "html")
if err != nil {
fmt.Printf("Error formatting as HTML: %v\n", err)
return
}
_ = html
// Save to file: os.WriteFile("diff.html", []byte(html), 0644)
Example (PerformanceWithDiff) ¶
// Test with large structures
type BigStruct struct {
ID int
Data map[string]any
Nested []map[string][]int
}
// Generate large test data
n := 1000
big1 := &BigStruct{
ID: 1,
Data: make(map[string]any),
Nested: make([]map[string][]int, n),
}
big2 := &BigStruct{
ID: 1,
Data: make(map[string]any),
Nested: make([]map[string][]int, n),
}
for i := range n {
key := fmt.Sprintf("key_%d", i)
big1.Data[key] = map[string]any{
"value": i,
"array": make([]int, 10),
}
big2.Data[key] = map[string]any{
"value": i,
"array": make([]int, 10),
}
big1.Nested[i] = map[string][]int{
"a": {i, i * 2, i * 3},
"b": {i, i * 4, i * 5},
}
big2.Nested[i] = map[string][]int{
"a": {i, i * 2, i * 3},
"b": {i, i * 4, i * 5},
}
}
// Make one change
big2.Data["key_500"] = map[string]any{
"value": 999, // Changed
"array": make([]int, 10),
}
// Compare with diff limiting
result := CompareWithDiff(big1, big2,
WithMaxDepth(5),
WithDiffMode(DiffModeFull),
WithMaxDiffs(100), // Stop after 100 differences
)
fmt.Printf("Found %d differences (limited to first 100)\n", len(result.Differences))
fmt.Printf("Total nodes examined: %d\n", result.PathStats.TotalNodes)
Example (StringLists) ¶
Example_stringLists demonstrates comparing two lists of strings with various options like order-independent comparison.
// Basic string list comparison
list1 := []string{"apple", "banana", "cherry", "date"}
list2 := []string{"apple", "banana", "cherry", "date"}
comp := New()
if comp.Equal(list1, list2) {
fmt.Println("Lists are equal")
}
// Lists with different order - strict comparison
list3 := []string{"banana", "apple", "cherry", "date"}
if !comp.Equal(list1, list3) {
fmt.Println("Lists with different order are not equal (strict mode)")
}
// Lists with different order - order-independent comparison
compIgnoreOrder := NewWithOptions(IgnoreSliceOrder())
if compIgnoreOrder.Equal(list1, list3) {
fmt.Println("Lists with different order are equal (ignore order mode)")
}
// Lists with differences
list4 := []string{"apple", "banana", "cherry", "elderberry"}
diffComp := New()
diffs, err := diffComp.Diff(list1, list4)
if err != nil {
fmt.Printf("Error getting diff: %v\n", err)
return
}
if len(diffs) > 0 {
fmt.Printf("Found %d differences\n", len(diffs))
for _, diff := range diffs {
if diff.Path != "" { // Skip summary messages
fmt.Printf(" - %s: %s\n", diff.Path, diff.Message)
}
}
}
// Empty and nil lists
var nilList []string
emptyList := []string{}
compEquateEmpty := NewWithOptions(EquateEmpty())
if compEquateEmpty.Equal(nilList, emptyList) {
fmt.Println("Nil and empty lists are equal (with EquateEmpty option)")
}
Output: Lists are equal Lists with different order are not equal (strict mode) Lists with different order are equal (ignore order mode) Found 2 differences - [3]: date != elderberry Nil and empty lists are equal (with EquateEmpty option)
Example (StringListsDiff) ¶
Example_stringListsDiff demonstrates detailed diff generation for string lists showing what changed.
// Original tags
oldTags := []string{"go", "backend", "api", "rest", "database"}
// Updated tags
newTags := []string{"go", "backend", "api", "graphql", "microservices"}
// Create a diff comparer
comp := NewDiffComparer(WithOutputFormat("markdown"))
// Get detailed differences
result := comp.CompareWithDiff(oldTags, newTags)
fmt.Printf("Equal: %v\n", result.Equal)
fmt.Printf("Summary: %s\n\n", result.Summary)
fmt.Println("Differences:")
for _, diff := range result.Differences {
fmt.Printf(" Path: %s\n", diff.Path)
fmt.Printf(" Expected: %v\n", diff.Detail.ExpectedValue)
fmt.Printf(" Actual: %v\n", diff.Detail.ActualValue)
fmt.Println()
}
// Get JSON Patch for programmatic updates
patch, err := comp.GetJSONPatch(oldTags, newTags)
if err != nil {
fmt.Printf("Error generating patch: %v\n", err)
return
}
patchJSON, err := json.MarshalIndent(patch, "", " ")
if err != nil {
fmt.Printf("Error marshaling patch: %v\n", err)
return
}
fmt.Printf("JSON Patch:\n%s\n", string(patchJSON))
// Output format will show:
// Equal: false
// Summary: Found X differences...
// And detailed path-by-path changes
Example (StringListsWithDuplicates) ¶
Example_stringListsWithDuplicates shows how to compare lists that may contain duplicate values.
list1 := []string{"apple", "banana", "apple", "cherry", "banana"}
list2 := []string{"banana", "apple", "cherry", "apple", "banana"}
// Strict order comparison
comp := New()
fmt.Printf("Equal (strict order): %v\n", comp.Equal(list1, list2))
// Order-independent comparison (duplicates must still match)
compIgnoreOrder := NewWithOptions(IgnoreSliceOrder())
fmt.Printf("Equal (ignore order): %v\n", compIgnoreOrder.Equal(list1, list2))
// Different number of duplicates
list3 := []string{"apple", "banana", "cherry", "apple"}
fmt.Printf("Equal with different duplicates: %v\n", compIgnoreOrder.Equal(list1, list3))
Output: Equal (strict order): false Equal (ignore order): true Equal with different duplicates: false
Index ¶
- Variables
- func ApplyJSONPatch(doc any, patch []JSONPatchOperation) (any, error)
- func AssertEqual(t TestingT, want, got any, opts ...Option) bool
- func AssertNotEqual(t TestingT, want, got any, opts ...Option) bool
- func DeepEqual(a, b any, opts ...Option) bool
- func DeepEqualT[T any](a, b T, opts ...Option) bool
- func Equal(a, b any, opts ...Option) bool
- func EqualCtx(ctx context.Context, a, b any, opts ...Option) (bool, error)
- func EqualT[T any](a, b T, opts ...Option) bool
- func RegisterFormatter(name string, fn Formatter) bool
- func RequireEqual(t TestingT, want, got any, opts ...Option)
- func UnregisterFormatter(name string) bool
- type Change
- type Chunk
- type Comparable
- type Comparator
- type Config
- type ContextComparator
- type DiffComparer
- type DiffMode
- type DiffResult
- type Difference
- type DifferenceDetail
- type Equatable
- type FieldNaming
- type Formatter
- type JSONPatchOperation
- type Option
- func EquateEmpty() Option
- func EquateNaNs() Option
- func IgnoreSliceOrder() Option
- func IgnoreStructFields(fields ...string) Option
- func IgnoreUnexported() Option
- func WithColorize(enable bool) Option
- func WithCustomComparator[T any](comparator func(a, b T) bool) Option
- func WithDiffMode(mode DiffMode) Option
- func WithEquateEmpty(enable bool) Option
- func WithFieldNaming(naming FieldNaming) Option
- func WithFloatPrecision(precision float64) Option
- func WithIgnoreMapKeys(keys ...string) Option
- func WithIgnorePathPatterns(patterns ...string) Option
- func WithIgnorePaths(paths ...string) Option
- func WithIncludeEqual(enable bool) Option
- func WithMaxDepth(depth int) Option
- func WithMaxDiffs(max int) Option
- func WithOutputFormat(format string) Option
- func WithReporter(reporter func(Difference)) Option
- func WithTimeLayout(layout string) Option
- type PathStats
- type TestingT
- type UnifiedDiff
- type VisualDiff
- type VisualNode
Examples ¶
- Package (ColorizedOutput)
- Package (ColorizedWithEqualValues)
- Package (DetailedDiff)
- Package (IncludeEqualValues)
- Package (JsonDiff)
- Package (PerformanceWithDiff)
- Package (StringLists)
- Package (StringListsDiff)
- Package (StringListsWithDuplicates)
- ApplyJSONPatch
- DeepEqual
- Equal
- Equal (Options)
- EqualCtx
- EqualT
- EquateEmpty
- EquateNaNs
- GetJSONPatch
- IgnoreSliceOrder
- IgnoreStructFields
- IgnoreUnexported
- New
- NewDiffComparer
- RegisterFormatter
- WithCustomComparator
- WithEquateEmpty
- WithFieldNaming
- WithFloatPrecision
- WithIgnoreMapKeys
- WithReporter
- WithTimeLayout
Constants ¶
This section is empty.
Variables ¶
var ( // ErrUnknownFormat is returned by FormatDiff when the requested format is // not a built-in format ("text", "json", "markdown", "html") and no // formatter has been registered for it with RegisterFormatter. ErrUnknownFormat = errors.New("comparator: unknown output format") // ErrCanceled is returned by the context-aware comparison functions and // methods when the provided context is canceled or its deadline is exceeded // before the comparison completes. It wraps the context's own error, so // errors.Is(err, context.Canceled) and errors.Is(err, context.DeadlineExceeded) // also report true. ErrCanceled = errors.New("comparator: comparison canceled") // ErrInvalidPatch is returned by ApplyJSONPatch when a patch operation is // malformed, references an unreachable path, or specifies an unsupported // operation. ErrInvalidPatch = errors.New("comparator: invalid JSON patch") )
Sentinel errors returned by the package. Use errors.Is to branch on them.
Functions ¶
func ApplyJSONPatch ¶
func ApplyJSONPatch(doc any, patch []JSONPatchOperation) (any, error)
ApplyJSONPatch applies an RFC 6902 JSON Patch to a document and returns the modified document. It supports the "add", "remove", "replace", "move", "copy", and "test" operations.
The document is first normalized to JSON values (objects become map[string]any and arrays become []any) by round-tripping through encoding/json, so doc must be JSON-serializable. The returned value uses those same JSON types.
Patches produced by GetJSONPatch use Go field names by default; generate them with WithFieldNaming(JSONTagNaming) when you intend to apply them to JSON-shaped documents so the pointers line up with the object keys.
A malformed operation, an unreachable path, or an unsupported op returns an error wrapping ErrInvalidPatch. A failed "test" operation also returns an error wrapping ErrInvalidPatch.
Example ¶
ExampleApplyJSONPatch applies a patch to a JSON-shaped document.
doc := map[string]any{"name": "Jon"}
patch := []JSONPatchOperation{
{Op: "replace", Path: "/name", Value: "John"},
{Op: "add", Path: "/email", Value: "john@example.com"},
}
got, err := ApplyJSONPatch(doc, patch)
if err != nil {
fmt.Println("error:", err)
return
}
m := got.(map[string]any)
fmt.Println(m["name"], m["email"])
Output: John john@example.com
func AssertEqual ¶
AssertEqual fails the test (via t.Errorf) when want and got are not equal under the given options, printing a readable diff of the differences. It returns true when the values are equal. Use it as a drop-in equality check in tests:
comparator.AssertEqual(t, wantUser, gotUser, comparator.IgnoreStructFields("ID"))
func AssertNotEqual ¶
AssertNotEqual fails the test when want and got are equal under the given options. It returns true when the values differ.
func DeepEqual ¶
DeepEqual is an alias for Equal, emphasizing that the comparison is deep (recursive). It provides the same functionality as Equal.
Example:
if comparator.DeepEqual(struct1, struct2) {
fmt.Println("Structs are deeply equal")
}
Example ¶
ExampleDeepEqual demonstrates DeepEqual, which behaves like Equal but keeps a name that is familiar to users of reflect.DeepEqual.
type point struct{ X, Y int }
fmt.Println(DeepEqual(point{1, 2}, point{1, 2}))
fmt.Println(DeepEqual(point{1, 2}, point{1, 3}))
Output: true false
func DeepEqualT ¶
DeepEqualT is an alias for EqualT, emphasizing the recursive comparison. It mirrors DeepEqual for callers who prefer that name.
func Equal ¶
Equal is a convenience function that creates a comparator and checks equality between two values. This is useful for one-off comparisons without explicitly creating a comparator instance.
Example:
if comparator.Equal(user1, user2, comparator.IgnoreStructFields("ID")) {
fmt.Println("Users are equal (ignoring ID)")
}
For multiple comparisons with the same configuration, create a comparator instance using New() or NewWithOptions() for better performance.
Example ¶
ExampleEqual demonstrates the package-level Equal convenience function for a quick deep comparison without constructing a Comparator.
a := map[string]int{"cpu": 2, "mem": 8}
b := map[string]int{"cpu": 2, "mem": 8}
fmt.Println(Equal(a, b))
fmt.Println(Equal(a, map[string]int{"cpu": 4, "mem": 8}))
Output: true false
Example (Options) ¶
ExampleEqual_options shows how options can be passed to the package-level helpers to relax the comparison.
left := []int{3, 1, 2}
right := []int{1, 2, 3}
fmt.Println(Equal(left, right))
fmt.Println(Equal(left, right, IgnoreSliceOrder()))
Output: false true
func EqualCtx ¶
EqualCtx is a package-level convenience for a one-off context-aware equality check.
Example ¶
ExampleEqualCtx shows a context-aware comparison that is canceled up front.
ctx, cancel := context.WithCancel(context.Background())
cancel()
_, err := EqualCtx(ctx, []int{1, 2, 3}, []int{1, 2, 3})
fmt.Println(err != nil)
Output: true
func EqualT ¶
EqualT reports whether two values of the same type are deeply equal. It is the type-safe counterpart of Equal: the compiler guarantees a and b share a type.
Example:
if comparator.EqualT(user1, user2, comparator.IgnoreStructFields("ID")) {
// ...
}
Example ¶
ExampleEqualT demonstrates the type-safe generic equality helper.
type point struct{ X, Y int }
fmt.Println(EqualT(point{1, 2}, point{1, 2}))
fmt.Println(EqualT(point{1, 2}, point{3, 4}))
Output: true false
func RegisterFormatter ¶
RegisterFormatter registers a custom formatter under the given name so that FormatDiff(result, name) and WithOutputFormat(name) can use it. The name is case-insensitive and must not collide with a built-in format ("text", "json", "markdown", "html"). Registering a nil formatter, an empty name, or a built-in name is a no-op that returns false.
RegisterFormatter is safe for concurrent use. Registering the same name again replaces the previous formatter.
Example ¶
ExampleRegisterFormatter adds a custom output format.
RegisterFormatter("summary-only", func(r *DiffResult) (string, error) {
if r.Equal {
return "equal", nil
}
return "different", nil
})
defer UnregisterFormatter("summary-only")
comp := NewDiffComparer()
result := comp.CompareWithDiff(1, 2)
out, _ := comp.FormatDiff(result, "summary-only")
fmt.Println(out)
Output: different
func RequireEqual ¶
RequireEqual behaves like AssertEqual but stops the test immediately (via Fatalf when t supports it) instead of merely marking it failed.
func UnregisterFormatter ¶
UnregisterFormatter removes a previously registered custom formatter. It returns true if a formatter was removed.
Types ¶
type Change ¶
type Change struct {
// Type indicates the change type: "add" (+), "remove" (-), or "context" ( )
Type string `json:"type"`
// Content is the line content
Content string `json:"content"`
// Line is the line number in the original or modified file
Line int `json:"line,omitempty"`
}
Change represents a single line change in a unified diff.
type Chunk ¶
type Chunk struct {
// Context describes the location of this chunk (e.g., "@@ -1,10 +1,12 @@")
Context string `json:"context"`
// Changes contains the individual line changes in this chunk
Changes []Change `json:"changes"`
}
Chunk represents a section of a unified diff, containing a group of related changes along with surrounding context lines.
type Comparable ¶
type Comparable[T any] interface { // CompareTo compares this instance with another instance of the same type. // Returns -1 if this < other, 0 if this == other, 1 if this > other. CompareTo(other T) int }
Comparable is a generic interface for types that can compare themselves to other instances of the same type. Implementations should return:
- negative integer if the receiver is less than other
- zero if the receiver equals other
- positive integer if the receiver is greater than other
This interface is useful for implementing custom ordering and sorting logic.
type Comparator ¶
type Comparator interface {
// Equal performs a deep equality check between two values using default configuration.
// Returns true if the values are deeply equal, false otherwise.
Equal(a, b any) bool
// EqualWithConfig performs a deep equality check using a custom configuration.
// This allows fine-grained control over comparison behavior such as float precision,
// slice ordering, and field ignoring.
EqualWithConfig(a, b any, config *Config) bool
// Diff returns a list of all differences found between two values.
// Each difference includes the path, type, values, and contextual information.
// Returns an error if the comparison process encounters an issue.
Diff(a, b any) ([]Difference, error)
}
Comparator is the main comparison interface that provides deep equality checking and difference detection for any Go values. It supports various data types including primitives, structs, slices, maps, pointers, and complex nested structures.
The comparator performs deep recursive comparison with cycle detection to handle self-referential data structures safely. It can be configured with various options to customize comparison behavior.
Example usage:
comp := comparator.New()
if comp.Equal(obj1, obj2) {
fmt.Println("Objects are equal")
}
func New ¶
func New() Comparator
New creates a new Comparator with default configuration. The default comparator uses:
- Float precision: 1e-9
- Equate empty values: true
- Equate NaNs: false
- Max depth: unlimited (0)
- Time layout: RFC3339Nano
Example:
comp := comparator.New()
if comp.Equal(obj1, obj2) {
fmt.Println("Objects are equal")
}
Example ¶
comp := New()
fmt.Println(comp.Equal(42, 42))
fmt.Println(comp.Equal("left", "right"))
Output: true false
func NewWithOptions ¶
func NewWithOptions(opts ...Option) Comparator
NewWithOptions creates a new Comparator with custom options. Options are applied in the order provided. Later options override earlier ones.
Example:
comp := comparator.NewWithOptions(
comparator.WithFloatPrecision(1e-6),
comparator.IgnoreSliceOrder(),
comparator.IgnoreStructFields("ID", "CreatedAt"),
)
if comp.Equal(user1, user2) {
fmt.Println("Users are equal (ignoring ID and CreatedAt)")
}
type Config ¶
type Config struct {
// contains filtered or unexported fields
}
Config holds all configuration options for comparison operations. It controls how values are compared, what differences are reported, and how output is formatted. Use Option functions to create and modify Config instances rather than directly manipulating fields.
The zero value is not safe to use; always create Config instances using defaultConfig() or through New/NewWithOptions functions.
type ContextComparator ¶
type ContextComparator interface {
// EqualCtx behaves like Comparator.Equal but honors ctx cancellation.
EqualCtx(ctx context.Context, a, b any) (bool, error)
// DiffCtx behaves like Comparator.Diff but honors ctx cancellation.
DiffCtx(ctx context.Context, a, b any) ([]Difference, error)
// CompareWithDiffCtx behaves like DiffComparer.CompareWithDiff but honors
// ctx cancellation.
CompareWithDiffCtx(ctx context.Context, a, b any) (*DiffResult, error)
}
ContextComparator is implemented by the comparators returned from New, NewWithOptions, and NewDiffComparer. Its methods accept a context.Context so long-running comparisons over large structures can be canceled or bounded by a deadline.
A canceled context causes the comparison to stop early and return an error that wraps both ErrCanceled and the context's own error.
type DiffComparer ¶
type DiffComparer interface {
Comparator
// CompareWithDiff performs a comprehensive comparison and returns a detailed result
// containing all differences, statistics, and a summary.
CompareWithDiff(a, b any) *DiffResult
// GetUnifiedDiff generates a unified diff format output similar to the Unix diff utility.
// This is useful for displaying differences in a familiar, line-oriented format.
GetUnifiedDiff(a, b any) (*UnifiedDiff, error)
// GetJSONPatch generates a JSON Patch (RFC 6902) document describing the differences.
// The patch can be applied to transform the first value into the second value.
GetJSONPatch(a, b any) ([]JSONPatchOperation, error)
// GetVisualDiff generates a tree-based visual representation of the differences.
// This is useful for creating graphical diff viewers or hierarchical displays.
GetVisualDiff(a, b any) (*VisualDiff, error)
// FormatDiff formats a diff result according to the specified format.
// Supported formats: "text", "json", "markdown", "html".
FormatDiff(result *DiffResult, format string) (string, error)
}
DiffComparer extends the Comparator interface with advanced diffing capabilities. It provides multiple output formats for differences including unified diff, JSON Patch, and visual tree representations. This interface is ideal for generating human-readable or machine-processable diff reports.
Example usage:
diffComp := comparator.NewDiffComparer(
comparator.WithOutputFormat("markdown"),
comparator.WithMaxDiffs(100),
)
result := diffComp.CompareWithDiff(obj1, obj2)
fmt.Println(result.Summary)
func NewDiffComparer ¶
func NewDiffComparer(opts ...Option) DiffComparer
NewDiffComparer creates a new DiffComparer with custom options. A DiffComparer provides all Comparator functionality plus advanced diff reporting capabilities.
Example:
diffComp := comparator.NewDiffComparer(
comparator.WithOutputFormat("markdown"),
comparator.WithMaxDiffs(100),
comparator.WithDiffMode(comparator.DiffModeUnified),
)
result := diffComp.CompareWithDiff(config1, config2)
if !result.Equal {
formatted, _ := diffComp.FormatDiff(result, "markdown")
fmt.Println(formatted)
}
Example ¶
type serviceConfig struct {
Host string
Port int
}
left := serviceConfig{Host: "localhost", Port: 8080}
right := serviceConfig{Host: "localhost", Port: 9090}
comp := NewDiffComparer()
result := comp.CompareWithDiff(left, right)
fmt.Println(result.Equal)
fmt.Println(len(result.Differences) > 0)
Output: false true
type DiffMode ¶
type DiffMode int
DiffMode specifies the format and detail level for reporting differences. Different modes are suitable for different use cases, from simple boolean checks to detailed patch operations.
const ( // DiffModeSimple provides basic difference reporting with minimal detail. // Suitable for quick equality checks with simple diff messages. DiffModeSimple DiffMode = iota // DiffModeFull provides comprehensive difference reporting with full context, // including nested differences and detailed statistics. DiffModeFull // DiffModeUnified generates differences in unified diff format, // similar to the Unix diff utility output. DiffModeUnified // DiffModeJSONPatch generates differences as JSON Patch operations (RFC 6902), // suitable for programmatic application of changes. DiffModeJSONPatch // DiffModeVisual generates a tree-based visual representation of differences, // suitable for graphical diff viewers. DiffModeVisual )
type DiffResult ¶
type DiffResult struct {
// Summary is a human-readable summary of the comparison result
Summary string `json:"summary,omitempty"`
// Differences contains all differences found (empty if Equal is true)
Differences []Difference `json:"differences,omitempty"`
// PathStats provides statistics about the comparison process
PathStats PathStats `json:"stats"`
// Equal indicates whether the two values are deeply equal
Equal bool `json:"equal"`
}
DiffResult holds the complete result of a comparison operation. It includes equality status, all differences found, statistics about the comparison process, and a human-readable summary.
Example:
result := comp.CompareWithDiff(obj1, obj2)
if !result.Equal {
fmt.Printf("Found %d differences\n", len(result.Differences))
fmt.Println(result.Summary)
for _, diff := range result.Differences {
fmt.Printf(" %s: %s\n", diff.Path, diff.Message)
}
}
func CompareWithDiff ¶
func CompareWithDiff(a, b any, opts ...Option) *DiffResult
CompareWithDiff is a convenience function that creates a diff comparator and performs a comprehensive comparison. It returns a detailed DiffResult containing all differences, statistics, and a summary.
This is useful for one-off detailed comparisons without explicitly creating a comparator instance.
Example:
result := comparator.CompareWithDiff(
expectedConfig,
actualConfig,
comparator.WithOutputFormat("markdown"),
comparator.WithMaxDiffs(50),
)
if !result.Equal {
fmt.Println(result.Summary)
for _, diff := range result.Differences {
fmt.Printf(" %s: %s\n", diff.Path, diff.Message)
}
}
For multiple comparisons, create a DiffComparer instance using NewDiffComparer() for better performance.
func CompareWithDiffCtx ¶
CompareWithDiffCtx is a package-level convenience for a one-off context-aware comprehensive comparison.
func CompareWithDiffT ¶
func CompareWithDiffT[T any](a, b T, opts ...Option) *DiffResult
CompareWithDiffT performs a comprehensive comparison between two values of the same type and returns a detailed result. It is the type-safe counterpart of CompareWithDiff.
type Difference ¶
type Difference struct {
// Path is the location of the difference using dot notation (e.g., "User.Address.City")
Path string `json:"path"`
// Message is a human-readable description of the difference
Message string `json:"message"`
// Severity classifies the importance: "error", "warning", "info"
Severity string `json:"severity"`
// Detail contains granular information about the difference
Detail DifferenceDetail `json:"detail"`
// Suggestions provides optional recommendations for resolving the difference
Suggestions []string `json:"suggestions,omitempty"`
// Level indicates the depth in the structure where this difference was found (0 = root)
Level int `json:"level"`
}
Difference represents a single difference found during comparison. It includes the location (path), depth level, description, detailed information, severity classification, and optional suggestions for resolution.
The Path field uses dot notation for nested structures:
- "User.Name" for struct fields
- "Users[0]" for slice elements
- "Config[key]" for map entries
type DifferenceDetail ¶
type DifferenceDetail struct {
// Type describes the kind of difference:
// "value_different", "type_mismatch", "missing", "extra_element",
// "length_mismatch", "missing_key", "extra_key", "nil_mismatch"
Type string `json:"type"`
// ExpectedType is the string representation of the expected value's type
ExpectedType string `json:"expected_type,omitempty"`
// ActualType is the string representation of the actual value's type
ActualType string `json:"actual_type,omitempty"`
// ExpectedValue is the expected value (first argument to comparison)
ExpectedValue any `json:"expected_value,omitempty"`
// ActualValue is the actual value (second argument to comparison)
ActualValue any `json:"actual_value,omitempty"`
// Children contains nested differences for complex structures
Children []Difference `json:"children,omitempty"`
}
DifferenceDetail provides granular, type-specific information about a difference. It includes the type of difference, the expected and actual types and values, and any nested differences for complex structures.
type Equatable ¶
type Equatable[T any] interface { // Equals returns true if this instance is considered equal to the other instance. Equals(other T) bool }
Equatable is a generic interface for types that can determine equality with other instances of the same type. This provides a type-safe way to implement custom equality logic.
Example implementation:
type Person struct {
ID int
Name string
}
func (p Person) Equals(other Person) bool {
return p.ID == other.ID
}
type FieldNaming ¶
type FieldNaming int
FieldNaming selects how struct fields are named in difference paths and in JSON Patch pointers.
const ( // GoFieldNaming uses the Go struct field name (e.g. "Name"). This is the // default and preserves backward compatibility. GoFieldNaming FieldNaming = iota // JSONTagNaming uses the name from the field's `json` struct tag when // present, falling back to the Go field name otherwise. This makes JSON // Patch pointers align with the JSON representation of the values. JSONTagNaming )
type Formatter ¶
type Formatter func(result *DiffResult) (string, error)
Formatter renders a DiffResult into a string representation. Register one with RegisterFormatter to teach FormatDiff a new output format (for example CSV, JUnit, or SARIF) without changing the package.
type JSONPatchOperation ¶
type JSONPatchOperation struct {
// Op is the operation type: "add", "remove", "replace", "move", "copy", or "test"
Op string `json:"op"`
// Path is the JSON Pointer (RFC 6901) to the target location
Path string `json:"path"`
// Value is the value to add, replace, or test (omitted for remove and move)
Value any `json:"value,omitempty"`
// From is the source path for move and copy operations
From string `json:"from,omitempty"`
}
JSONPatchOperation represents a single operation in a JSON Patch document (RFC 6902). JSON Patch is a format for describing changes to a JSON document. It can be used to apply partial updates, avoiding the need to send the entire document.
Example operations:
- {"op": "replace", "path": "/name", "value": "John"}
- {"op": "add", "path": "/tags/-", "value": "new-tag"}
- {"op": "remove", "path": "/deprecated"}
- {"op": "move", "from": "/old", "path": "/new"}
func GetJSONPatch ¶
func GetJSONPatch(a, b any, opts ...Option) ([]JSONPatchOperation, error)
GetJSONPatch is a convenience function that creates a diff comparator and generates a JSON Patch (RFC 6902) document describing the differences.
This is useful for one-off patch generation without explicitly creating a comparator instance.
Example:
patch, err := comparator.GetJSONPatch(oldDoc, newDoc)
if err != nil {
log.Fatal(err)
}
patchJSON, _ := json.MarshalIndent(patch, "", " ")
fmt.Println(string(patchJSON))
// Output:
// [
// {"op": "replace", "path": "/name", "value": "John"},
// {"op": "add", "path": "/email", "value": "john@example.com"}
// ]
For multiple patch generations, create a DiffComparer instance using NewDiffComparer() for better performance.
Example ¶
ExampleGetJSONPatch generates an RFC 6902 JSON Patch describing how to turn the first document into the second.
type profile struct {
Name string `json:"name"`
}
patch, err := GetJSONPatch(profile{Name: "Jon"}, profile{Name: "John"})
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Println(len(patch))
fmt.Println(patch[0].Op)
fmt.Println(patch[0].Path)
Output: 1 replace /Name
func GetJSONPatchT ¶
func GetJSONPatchT[T any](a, b T, opts ...Option) ([]JSONPatchOperation, error)
GetJSONPatchT generates an RFC 6902 JSON Patch describing how to turn a into b, for values of the same type. It is the type-safe counterpart of GetJSONPatch.
type Option ¶
type Option func(*Config)
Option is a function type that configures a Config instance. Options follow the functional options pattern, allowing flexible and readable configuration of comparators.
Example usage:
comp := comparator.NewWithOptions(
comparator.WithFloatPrecision(1e-6),
comparator.IgnoreSliceOrder(),
comparator.WithMaxDepth(10),
)
func EquateEmpty ¶
func EquateEmpty() Option
EquateEmpty configures the comparator to treat nil and empty values as equal. When enabled:
- nil slice equals empty slice ([]int(nil) == []int{})
- nil map equals empty map (map[string]int(nil) == map[string]int{})
- nil pointer equals pointer to zero value
- empty string equals empty string
This is enabled by default. Use this option explicitly when creating custom configurations to ensure consistent behavior.
Example ¶
ExampleEquateEmpty treats nil and empty containers as equal.
comp := NewWithOptions(EquateEmpty())
var nilSlice []string
emptySlice := []string{}
fmt.Println(comp.Equal(nilSlice, emptySlice))
Output: true
func EquateNaNs ¶
func EquateNaNs() Option
EquateNaNs configures the comparator to treat NaN (Not-a-Number) values as equal. By default, NaN != NaN according to IEEE 754 standard. Enable this option to consider all NaN values equal to each other.
This is useful in scientific computing or when NaN represents "missing data" or "undefined" consistently across both values.
Example ¶
ExampleEquateNaNs treats NaN values as equal, which the IEEE-754 standard does not do by default.
comp := NewWithOptions(EquateNaNs()) nan := math.NaN() fmt.Println(comp.Equal(nan, nan))
Output: true
func IgnoreSliceOrder ¶
func IgnoreSliceOrder() Option
IgnoreSliceOrder configures the comparator to treat slices as sets, ignoring the order of elements. When enabled, [1, 2, 3] will be considered equal to [3, 1, 2].
This is useful when comparing collections where order doesn't matter, such as sets represented as slices or unordered query results.
Note: For simple types, elements are sorted before comparison. For complex types, a more expensive matching algorithm is used.
Example ¶
comp := NewWithOptions(IgnoreSliceOrder())
left := []string{"api", "worker", "db"}
right := []string{"db", "api", "worker"}
fmt.Println(comp.Equal(left, right))
Output: true
func IgnoreStructFields ¶
IgnoreStructFields configures the comparator to skip specified struct fields by name. This applies to all structs encountered during comparison.
Useful for ignoring fields like timestamps, IDs, or other fields that are expected to differ but shouldn't affect equality.
Example:
comp := comparator.NewWithOptions(
comparator.IgnoreStructFields("CreatedAt", "UpdatedAt", "ID"),
)
Example ¶
ExampleIgnoreStructFields skips volatile fields such as IDs and timestamps so that two otherwise-identical records compare as equal.
type user struct {
ID int
Name string
UpdatedAt string
}
comp := NewWithOptions(IgnoreStructFields("ID", "UpdatedAt"))
u1 := user{ID: 1, Name: "Alice", UpdatedAt: "2026-01-01"}
u2 := user{ID: 2, Name: "Alice", UpdatedAt: "2026-07-23"}
fmt.Println(comp.Equal(u1, u2))
Output: true
func IgnoreUnexported ¶
func IgnoreUnexported() Option
IgnoreUnexported configures the comparator to skip unexported struct fields. By default, unexported fields are compared if accessible through reflection. Enable this option to only compare exported (public) fields.
This is useful when comparing structs where unexported fields may contain internal state that shouldn't affect equality, or when unexported fields are not accessible.
Example ¶
ExampleIgnoreUnexported ignores unexported struct fields during comparison.
type credential struct {
Name string
token string // unexported
}
comp := NewWithOptions(IgnoreUnexported())
c1 := credential{Name: "svc", token: "aaa"}
c2 := credential{Name: "svc", token: "bbb"}
fmt.Println(comp.Equal(c1, c2))
Output: true
func WithColorize ¶
WithColorize enables colored output for text format diffs. When enabled, differences are highlighted using ANSI color codes:
- Green: added values
- Red: removed values
- Yellow: warnings
- Cyan: paths and labels
This option only affects text format output. Other formats (JSON, HTML, Markdown) have their own styling mechanisms.
Example:
comp := comparator.NewDiffComparer(
comparator.WithColorize(true),
comparator.WithOutputFormat("text"),
)
func WithCustomComparator ¶
WithCustomComparator registers a custom comparison function for a specific type T. The comparator function receives two values of type T and returns true if they should be considered equal.
Custom comparators take precedence over default comparison logic for the specified type. This is useful for types with special equality semantics or when you need to override default behavior.
Example:
type User struct {
ID int
Name string
Age int
}
comp := comparator.NewWithOptions(
comparator.WithCustomComparator(func(a, b User) bool {
return a.ID == b.ID // Compare users by ID only
}),
)
Example ¶
ExampleWithCustomComparator registers domain-specific comparison logic for a concrete type.
type account struct {
ID int
Name string
}
// Two accounts are considered equal when they share the same ID.
comp := NewWithOptions(WithCustomComparator(func(a, b account) bool {
return a.ID == b.ID
}))
a1 := account{ID: 7, Name: "prod"}
a2 := account{ID: 7, Name: "production"}
fmt.Println(comp.Equal(a1, a2))
Output: true
func WithDiffMode ¶
WithDiffMode sets the diff reporting mode. See DiffMode constants for available modes and their descriptions.
The default mode is DiffModeSimple.
Example:
comp := comparator.NewDiffComparer(
comparator.WithDiffMode(comparator.DiffModeUnified),
)
func WithEquateEmpty ¶
WithEquateEmpty controls whether nil and empty containers (slices and maps) are treated as equal. Unlike EquateEmpty, which can only enable the behavior, this option can also disable it (the default is enabled).
Example:
// Treat nil and empty as distinct. comp := comparator.NewWithOptions(comparator.WithEquateEmpty(false))
Example ¶
ExampleWithEquateEmpty shows disabling the default nil/empty equivalence.
var nilSlice []int
emptySlice := []int{}
fmt.Println(NewWithOptions(WithEquateEmpty(true)).Equal(nilSlice, emptySlice))
fmt.Println(NewWithOptions(WithEquateEmpty(false)).Equal(nilSlice, emptySlice))
Output: true false
func WithFieldNaming ¶
func WithFieldNaming(naming FieldNaming) Option
WithFieldNaming selects how struct field names appear in difference paths and JSON Patch pointers. The default is GoFieldNaming. Use JSONTagNaming to make paths and patches align with the JSON representation of the values.
Example:
comp := comparator.NewDiffComparer(comparator.WithFieldNaming(comparator.JSONTagNaming)) // A field `Name string json:"name"` produces the pointer "/name".
Example ¶
ExampleWithFieldNaming shows JSON-tag-aware JSON Patch pointers.
type profile struct {
FullName string `json:"full_name"`
}
patch, _ := GetJSONPatchT(
profile{FullName: "Jon"},
profile{FullName: "John"},
WithFieldNaming(JSONTagNaming),
)
fmt.Println(patch[0].Op, patch[0].Path)
Output: replace /full_name
func WithFloatPrecision ¶
WithFloatPrecision sets the precision threshold for floating-point comparisons. Two float values are considered equal if their absolute difference is less than or equal to the specified precision.
The default precision is 1e-9. Use a larger value for less strict comparisons, or a smaller value for more strict comparisons.
Example:
comp := comparator.NewWithOptions(
comparator.WithFloatPrecision(1e-6),
)
// 1.0000001 and 1.0000002 will be considered equal
Example ¶
ExampleWithFloatPrecision shows how to treat floating-point values that are close enough as equal.
comp := NewWithOptions(WithFloatPrecision(1e-3)) fmt.Println(comp.Equal(0.1+0.2, 0.3))
Output: true
func WithIgnoreMapKeys ¶
WithIgnoreMapKeys skips the named map keys anywhere they appear during comparison. Keys are matched by their default string representation, so non-string keys such as integers are matched by their formatted value.
Example:
comp := comparator.NewWithOptions(comparator.WithIgnoreMapKeys("updatedAt", "etag"))
Example ¶
ExampleWithIgnoreMapKeys ignores volatile map keys.
a := map[string]int{"value": 1, "updatedAt": 100}
b := map[string]int{"value": 1, "updatedAt": 999}
fmt.Println(NewWithOptions(WithIgnoreMapKeys("updatedAt")).Equal(a, b))
Output: true
func WithIgnorePathPatterns ¶
WithIgnorePathPatterns skips struct fields whose canonical path matches any of the given regular expressions. Invalid patterns are ignored so option construction never panics; use regexp.Compile beforehand if you need to validate patterns.
Example:
// Ignore every field named "password" at any depth. comp := comparator.NewWithOptions(comparator.WithIgnorePathPatterns(`(^|\.)[Pp]assword$`))
func WithIgnorePaths ¶
WithIgnorePaths skips struct fields at the given canonical paths. Paths use dotted field names with bracketed index or key segments kept inline, matching the form reported in Difference.Path, e.g. "User.Address.Zip".
Example:
comp := comparator.NewWithOptions(comparator.WithIgnorePaths("Meta.UpdatedAt", "Meta.Etag"))
func WithIncludeEqual ¶
WithIncludeEqual configures the comparator to include equal values in the diff output. By default, only differences are reported. When enabled, the diff will also show paths where values are equal, providing a complete picture of the comparison.
This is useful for:
- Generating comprehensive comparison reports
- Debugging comparison logic
- Creating detailed audit trails
Note: Enabling this option may significantly increase output size for large structures.
Example:
comp := comparator.NewDiffComparer(
comparator.WithIncludeEqual(true),
)
func WithMaxDepth ¶
WithMaxDepth sets the maximum recursion depth for comparisons. When the specified depth is reached, the comparator performs a shallow comparison instead of recursing further.
A depth of 0 (default) means unlimited depth. Use this option to prevent stack overflows with deeply nested structures or to improve performance by limiting comparison depth.
Example:
comp := comparator.NewWithOptions(
comparator.WithMaxDepth(5), // Only compare 5 levels deep
)
func WithMaxDiffs ¶
WithMaxDiffs sets the maximum number of differences to collect. Once this limit is reached, no more differences will be recorded. This helps prevent memory issues when comparing large structures with many differences.
The default is 1000 differences. Set to 0 for unlimited (use with caution).
Example:
comp := comparator.NewDiffComparer(
comparator.WithMaxDiffs(50), // Only collect first 50 differences
)
func WithOutputFormat ¶
WithOutputFormat sets the output format for formatted diffs. Supported formats:
- "text": Plain text output (default)
- "json": JSON format
- "markdown": Markdown format
- "html": HTML format
Example:
comp := comparator.NewDiffComparer(
comparator.WithOutputFormat("markdown"),
)
func WithReporter ¶
func WithReporter(reporter func(Difference)) Option
WithReporter registers a callback invoked for every difference as it is discovered during CompareWithDiff, Diff, and the Get*Diff methods. It is useful for streaming, logging, or integrating with test frameworks without waiting for the full result to be materialized.
The reporter is called in traversal order and must not retain the Difference beyond the callback if the caller mutates shared state.
Example:
comp := comparator.NewDiffComparer(comparator.WithReporter(func(d comparator.Difference) {
log.Printf("%s: %s", d.Path, d.Message)
}))
Example ¶
ExampleWithReporter streams differences via a callback.
type cfg struct{ A, B int }
count := 0
comp := NewDiffComparer(WithReporter(func(Difference) { count++ }))
comp.CompareWithDiff(cfg{1, 2}, cfg{1, 3})
fmt.Println(count > 0)
Output: true
func WithTimeLayout ¶
WithTimeLayout sets the time format layout for time.Time comparisons. This is currently used for formatting but doesn't affect equality checks (time.Time values use the native Equal method).
The default layout is time.RFC3339Nano.
Example:
comp := comparator.NewWithOptions(
comparator.WithTimeLayout(time.RFC3339),
)
Example ¶
ExampleWithTimeLayout controls how time.Time values are rendered in diff output, while equality still uses the native time comparison.
comp := NewWithOptions(WithTimeLayout(time.RFC3339)) t1 := time.Date(2026, 7, 23, 10, 0, 0, 0, time.UTC) t2 := time.Date(2026, 7, 23, 10, 0, 0, 0, time.UTC) fmt.Println(comp.Equal(t1, t2))
Output: true
type PathStats ¶
type PathStats struct {
// TotalNodes is the total number of nodes encountered during traversal
TotalNodes int `json:"total_nodes"`
// ComparedNodes is the number of nodes that were actually compared
ComparedNodes int `json:"compared_nodes"`
// DifferentNodes is the number of nodes found to be different
DifferentNodes int `json:"different_nodes"`
// IgnoredNodes is the number of nodes skipped (e.g., due to field ignore rules)
IgnoredNodes int `json:"ignored_nodes"`
}
PathStats provides statistical information about the comparison operation. It tracks how many nodes were examined, compared, found different, or ignored.
These statistics are useful for understanding the scope and efficiency of the comparison, especially for large or complex data structures.
type TestingT ¶
TestingT is the subset of *testing.T (and *testing.B) used by the assertion helpers. Accepting an interface avoids importing the testing package into production builds and lets callers supply their own compatible type.
type UnifiedDiff ¶
type UnifiedDiff struct {
// Header contains the diff header (e.g., "--- a\n+++ b\n")
Header string `json:"header"`
// Chunks contains the individual diff chunks with context
Chunks []Chunk `json:"chunks"`
}
UnifiedDiff represents differences in unified diff format, similar to the output of the Unix diff utility. This format is familiar to developers and integrates well with version control systems.
The format uses '+' and '-' prefixes to indicate additions and removals.
func (*UnifiedDiff) String ¶
func (u *UnifiedDiff) String() string
String renders a unified diff in the familiar text form: the header followed by each hunk's context header and its change lines. Change contents already carry their "+", "-", or space prefix.
type VisualDiff ¶
type VisualDiff struct {
// Root is the root node of the visual diff tree
Root *VisualNode `json:"root"`
}
VisualDiff represents a tree-based visual representation of differences. This structure is suitable for building graphical diff viewers or hierarchical displays in user interfaces.
Each node in the tree contains information about its path, value, status (same/different/added/removed), and child nodes.
func (*VisualDiff) String ¶
func (v *VisualDiff) String() string
String renders a visual diff tree as an indented, one-node-per-line string. Each node is prefixed by a status symbol: "+" added, "-" removed, "~" different, and a space for unchanged nodes.
type VisualNode ¶
type VisualNode struct {
// Path is the full path to this node (e.g., "root.users[0].name")
Path string `json:"path"`
// Value is the string representation of this node's value
Value string `json:"value"`
// Status indicates the state: "same", "different", "added", "removed"
Status string `json:"status"`
// Children contains child nodes for nested structures
Children []*VisualNode `json:"children,omitempty"`
}
VisualNode represents a single node in a visual diff tree.