Fix

"invalid operation: struct comparison" in Go

Fix 'invalid operation: struct comparison' in Go by using reflect.DeepEqual or comparing individual fields instead of the whole struct.

The compiler rejects your struct comparison

You define a struct with a slice, try to compare two instances with ==, and the build fails. The error message is blunt: invalid operation: a == b (struct containing []string cannot be compared). You've been coding in Python or JavaScript where comparing objects just works. Go is drawing a line in the sand.

This error happens because the struct contains a field that Go considers incomparable. Slices, maps, and functions fall into this category. If a struct holds any of these types, the struct itself becomes incomparable. The compiler enforces this rule at build time. You cannot compare the structs until you remove the incomparable fields or change how you check for equality.

Why Go bans the comparison

Go refuses to guess what you mean by "equal" when references are involved. A slice is not the data itself. A slice is a descriptor that points to an underlying array. It contains a pointer to the memory, a length, and a capacity. If Go allowed == on slices, it would have to choose between two behaviors: comparing the descriptors or comparing the content.

Comparing descriptors checks if two slices point to the exact same memory block. This is pointer equality. It is fast, but it is rarely what you want. Two slices can hold identical data in different memory locations and be considered unequal. Comparing content checks every element. This is deep equality. It is expensive, and it introduces ambiguity around edge cases like NaN values or cyclic references.

Go chooses safety and explicitness. The language forces you to make the decision. If you want pointer equality, you compare the pointers. If you want content equality, you write the code that walks the data. The compiler stops you from accidentally doing the wrong thing.

Maps and functions follow the same logic. A map is a reference to a hash table. Two maps can have the same keys and values but different internal structures. Functions are closures that may capture different environments. Comparing functions is undecidable in the general case. Go bans the comparison to keep the language predictable and performant.

The slice header reality

Understanding the slice header clarifies why the restriction exists. A slice value in Go is a small struct-like header passed by value. It looks roughly like this:

type sliceHeader struct {
    Data *byte // Pointer to the underlying array
    Len  int   // Number of elements
    Cap  int   // Capacity of the underlying array
}

When you assign a slice to another variable, you copy the header. The pointer, length, and capacity are duplicated. The underlying array is shared. If Go implemented == for slices, the most natural implementation would compare these three fields. That means []int{1, 2} and []int{1, 2} would be unequal if they lived in different arrays. That behavior confures almost every developer. Go removes the confusion by making the operation illegal.

func main() {
    s1 := []int{1, 2, 3}
    s2 := []int{1, 2, 3}
    
    // s1 and s2 have identical content but different headers.
    // s1.Data points to one array, s2.Data points to another.
    
    // This line is rejected by the compiler.
    // Error: invalid operation: s1 == s2 (slice can only be compared to nil)
    if s1 == s2 {
        fmt.Println("Equal")
    }
}

The compiler error slice can only be compared to nil appears if you try to compare slices directly. The slice type allows comparison against nil because that checks if the pointer is null. Any other comparison is forbidden. Structs inherit this restriction. If a field is incomparable, the whole struct is incomparable.

Fixing the comparison

You have three main paths to compare structs with slices, maps, or functions. The right choice depends on performance needs, complexity, and whether you are writing production code or tests.

Manual field comparison

The most robust approach is to compare the fields yourself. You write the logic that defines equality for your specific data. This is fast, explicit, and gives you full control. You can ignore fields that shouldn't affect equality, or apply custom rules to floating-point numbers.

type User struct {
    ID   int
    Name string
    Tags []string
}

// UsersEqual checks if two users are semantically identical.
func UsersEqual(a, b User) bool {
    // Compare simple fields directly.
    if a.ID != b.ID || a.Name != b.Name {
        return false
    }
    
    // Compare slice lengths first to avoid index out of range.
    if len(a.Tags) != len(b.Tags) {
        return false
    }
    
    // Iterate and compare each tag.
    for i := range a.Tags {
        if a.Tags[i] != b.Tags[i] {
            return false
        }
    }
    
    return true
}

This code is verbose, but it is efficient. It returns early on mismatch. It does not allocate memory. It does not use reflection. The community accepts this boilerplate because it makes the equality rules visible. If the definition of equality changes, the code changes in one place.

Using slices.Equal and maps.Equal

Go 1.21 introduced the slices and maps packages with dedicated comparison functions. These are better than reflect.DeepEqual for simple cases. They are type-safe, faster, and handle edge cases consistently.

import "slices"

func UsersEqualModern(a, b User) bool {
    // Compare scalar fields directly.
    if a.ID != b.ID || a.Name != b.Name {
        return false
    }
    
    // slices.Equal handles length and element comparison efficiently.
    // It returns true for nil and empty slices, treating them as equal.
    return slices.Equal(a.Tags, b.Tags)
}

slices.Equal compares elements using ==. It works for any element type that supports comparison. If the slice contains another incomparable type, slices.Equal will not compile. This pushes the problem down one level, which is often helpful. You can nest slices.Equal calls for complex structures.

Using reflect.DeepEqual

reflect.DeepEqual compares values recursively using reflection. It handles slices, maps, structs, and pointers automatically. It is convenient, but it comes with costs. Reflection is slower than manual code. It allocates memory. It can panic on certain types like channels. It also has surprising behavior with NaN and nil slices.

import "reflect"

func UsersEqualReflect(a, b User) bool {
    // reflect.DeepEqual walks the entire structure using runtime type info.
    // This is slower and allocates, but requires no boilerplate.
    return reflect.DeepEqual(a, b)
}

Use reflect.DeepEqual when you are writing test assertions or quick scripts where performance does not matter. It is a sledgehammer. It works for almost anything, but it hides the logic. If you need to compare structs in a hot loop, avoid it.

Real world: Config reload check

Imagine a server that reloads configuration from a file. You want to skip the reload if the new config matches the current one. The config contains a slice of allowed origins.

type Config struct {
    Port        int
    Origins     []string
    TimeoutSecs float64
}

// ConfigEqual checks if two configs are functionally identical.
// It uses slices.Equal for the slice and a tolerance check for the float.
func ConfigEqual(a, b Config) bool {
    if a.Port != b.Port {
        return false
    }
    
    // slices.Equal is type-safe and efficient for the slice field.
    if !slices.Equal(a.Origins, b.Origins) {
        return false
    }
    
    // Floating point comparison requires tolerance.
    // Direct comparison fails due to precision issues.
    diff := a.TimeoutSecs - b.TimeoutSecs
    if diff < 0 {
        diff = -diff
    }
    return diff < 0.001
}

This example shows why manual control matters. reflect.DeepEqual would treat 1.0 and 1.0000000000000002 as unequal, which might trigger unnecessary reloads. It would also treat a nil slice and an empty slice as equal, which might be correct or incorrect depending on your semantics. Writing the comparison function lets you encode the business rules.

Pitfalls and runtime traps

The compiler catches the basic error, but runtime issues lurk if you use reflection or custom logic carelessly.

reflect.DeepEqual panics if it encounters a channel. Channels are not comparable, and reflection cannot safely inspect their state. The runtime throws reflect.DeepEqual: unsupported type: chan int. If your struct contains a channel, DeepEqual will crash. You must exclude channels from comparison or use a custom method.

Floating-point NaN values break equality. NaN != NaN is always true. reflect.DeepEqual treats two NaN values as equal, which can be surprising. If your data includes NaN, you need to handle it explicitly. Manual comparison lets you decide whether NaN should match NaN or not.

Cyclic references can cause infinite loops in naive comparison code. reflect.DeepEqual tracks visited pointers to avoid loops, but custom code must do the same. If you write a recursive comparison for a graph structure, you risk a stack overflow. Use a visited map to track nodes you have already seen.

The compiler error invalid operation: a == b (struct containing map[string]int cannot be compared) appears for maps too. Maps are references. Two maps can have the same content but different internal hash tables. Go forbids == on maps for the same reason it forbids it on slices. You must use maps.Equal or manual iteration.

Decision matrix

Choose the comparison strategy based on your constraints. Each approach has a clear role.

Use manual field comparison when you need high performance and the struct has a small number of fields. Writing a.ID == b.ID && a.Name == b.Name is fast, explicit, and gives you full control over what "equal" means. The compiler inlines this code, and it allocates zero memory.

Use slices.Equal and maps.Equal when you need to compare collection fields in production code. These functions are type-safe, efficient, and handle length checks automatically. They integrate cleanly with manual comparison for scalar fields.

Use a custom Equal method when the comparison logic is complex or involves floating-point tolerance. Define a method on the struct to encapsulate the rules. The receiver name should be one or two letters, like (c Config) Equal(other Config) bool. This keeps the logic close to the data definition and makes the API discoverable.

Use reflect.DeepEqual when you are writing test code or a utility where performance does not matter. It handles nested structures without boilerplate, making it convenient for assertions. Accept the performance cost in exchange for developer velocity in non-critical paths.

Use a hash or checksum when you need to compare large data sets efficiently. Compute a hash of the relevant fields once and store it. Comparing hashes is a single integer comparison. This is ideal for caches or deduplication where you check equality thousands of times per second.

Where to go next

The compiler error is a guardrail. Build the bridge yourself. Define equality explicitly, and your code will be faster and clearer.