10.7 Comparing Structs: When == Works and When It Doesn't
Right, so you want to compare two structs. Your first instinct, the good ol’ == operator, is a solid one. It works perfectly… until it doesn’t. And when it doesn’t, it fails with a spectacularly unhelpful compiler error that essentially tells you, “You can’t compare these, and I’m not going to tell you why.” Let’s demystify that.
The golden rule is simple: you can use == and != on structs only if all their fields are themselves comparable. A field is comparable if you can use == on it. Think basic types: string, int, bool, etc., or arrays of those types, or other structs made entirely of comparable types. It’s comparability turtles all the way down.
Let’s start with the easy win.
When == Just Works
Here’s a simple Point struct. It’s made of nothing but int fields, which are absolutely comparable.
type Point struct {
X int
Y int
}
func main() {
p1 := Point{10, 20}
p2 := Point{10, 20}
p3 := Point{15, 5}
fmt.Println(p1 == p2) // true
fmt.Println(p1 == p3) // false
fmt.Println(p1 != p3) // true
}
This behaves exactly as you’d expect. The compiler can literally just generate code to compare p1.X == p2.X && p1.Y == p2.Y. No magic, no fuss.
When == Blows Up in Your Face
Now, let’s break it. Here’s a Person struct. Seems innocent, right?
type Person struct {
Name string
Age int
Tags []string // Oh no.
}
func main() {
alice1 := Person{"Alice", 30, []string{"admin", "go"}}
alice2 := Person{"Alice", 30, []string{"admin", "go"}}
fmt.Println(alice1 == alice2) // You wish.
}
Try to run this, and the compiler will stop you cold with: invalid operation: alice1 == alice2 (struct containing []string cannot be compared). The culprit? The []string field. Slices, maps, and functions are not comparable. Why? Because they are reference types; their value is essentially a pointer to an underlying data structure. Comparing two slices with == would only check if they point to the same underlying array, not if their contents are the same. The Go designers, rightly, decided this was a foot-gun waiting to happen and just disallowed it entirely.
The Workaround: reflect.DeepEqual
So, alice1 and alice2 are clearly logically equal, but the compiler won’t let you use ==. This is where you reach for the big, slow, and occasionally awkward hammer: reflect.DeepEqual.
import "reflect"
// ...
fmt.Println(reflect.DeepEqual(alice1, alice2)) // true
DeepEqual does exactly what you want here: it recursively compares the contents of everything. It will return true for our two Person structs. But be warned, it’s not a direct replacement for ==. It has… opinions.
For example, an empty slice ([]string{}) and a nil slice ([]string(nil)) are not the same thing to DeepEqual, even though you can often use them interchangeably in many Go operations. It’s incredibly precise, which is both its strength and its weakness. It’s also much slower than a simple == check, so don’t use it in a tight inner loop if you can avoid it.
Comparing Structs with Non-Comparable but Mutable Fields
This is a subtle one. Let’s look at a Config struct.
type Config struct {
Path string
Timeout time.Duration
Mutex sync.Mutex // Yikes.
}
Can you use == on this? Technically, no. sync.Mutex contains fields that are not comparable. But even if you could, should you? Absolutely not. A mutex’s state changes—it’s locked and unlocked. Comparing two mutexes makes no logical sense. This is a case where the language’s comparability rules are protecting you from your own bad ideas. For a struct like this, you’d define equality based on the comparable fields that actually matter for logical identity (probably Path and Timeout).
The Best Practice: Define a Custom .Equal() Method
For any non-trivial struct, the most robust and performant approach is to define your own method for equality. This gives you complete control.
type NetworkAddress struct {
IP net.IP // net.IP is a slice ([]byte), so not comparable.
Port int
}
// Equal defines our own concept of equality for NetworkAddress.
func (a NetworkAddress) Equal(b NetworkAddress) bool {
// Compare the Port, which is a simple int.
if a.Port != b.Port {
return false
}
// Compare the IPs. net.IP has an Equal method we can use.
// This is both efficient and semantically correct.
return a.IP.Equal(b.IP)
}
func main() {
addr1 := NetworkAddress{IP: net.ParseIP("192.168.1.1"), Port: 80}
addr2 := NetworkAddress{IP: net.ParseIP("192.168.1.1"), Port: 80}
fmt.Println(addr1.Equal(addr2)) // true
// fmt.Println(addr1 == addr2) // Still a compile-time error. Good.
}
This is the way. It’s explicit, it’s fast, and it allows you to encode exactly what “equal” means for your specific type, ignoring irrelevant fields like internal mutexes or cached data. You’re not fighting the language; you’re working with it.