Two maps can represent the same application state even when their values aren’t directly comparable with ==. One map might hold structs containing slices, another might use a different value type, or a text field might be considered equal regardless of letter case.
maps.EqualFunc handles that situation by keeping key comparison fixed while letting you define value equality. It is a compact option when the question is strictly whether two maps contain the same keys and equivalent values.
How maps.EqualFunc compares Go maps
The standard library exposes maps.EqualFunc with separate value types for the two maps:
func EqualFunc[M1 ~map[K]V1, M2 ~map[K]V2, K comparable, V1, V2 any](
m1 M1,
m2 M2,
eq func(V1, V2) bool,
) boolThe maps must use the same key type. Their value types may differ. For entries with the same key, eq decides whether the two values count as equal.
The function returns true only when both maps have the same number of entries, every key from the first map exists in the second, and each corresponding value passes the equality function. Keys still use normal Go map-key equality; the callback applies only to values.
That distinction matters. A callback cannot make "API" and "api" equivalent map keys. If key normalization is part of the requirement, normalize keys before comparison or use a different representation.
Compare maps with different value types
A useful case is checking current state against desired state when the two layers use different structs. The fields can have different names while still representing the same information.
package main
import (
"fmt"
"maps"
"strings"
)
type Current struct {
Label string
Enabled bool
}
type Desired struct {
Name string
Enabled bool
}
func main() {
current := map[string]Current{
"api": {Label: "PRIMARY", Enabled: true},
"jobs": {Label: "worker", Enabled: false},
}
desired := map[string]Desired{
"api": {Name: "primary", Enabled: true},
"jobs": {Name: "WORKER", Enabled: false},
}
same := maps.EqualFunc(current, desired, func(a Current, b Desired) bool {
return strings.EqualFold(a.Label, b.Name) &&
a.Enabled == b.Enabled
})
fmt.Println(same) // true
}The callback compares Current with Desired directly. There is no need to convert either entire map into an intermediate type just to perform the check.
This pattern is especially useful at boundaries between representations. Keep the callback narrow: it should encode the exact equality rule needed by the caller, not silently perform unrelated validation or mutation.
Compare values that cannot use ==
maps.Equal requires comparable values because it uses ==. Structs containing slices or maps are not comparable, so they cannot be passed to maps.Equal as map values.
maps.EqualFunc has no comparable constraint on V1 or V2. The callback can inspect those values using a suitable comparison rule.
For example, a map may store permission slices where order has no meaning. If the application guarantees that each slice is already sorted and contains no duplicates, slices.Equal can compare corresponding slices:
same := maps.EqualFunc(left, right, func(a, b []string) bool {
return slices.Equal(a, b)
})That example depends on the stated ordering guarantee. If []string{"read", "write"} and []string{"write", "read"} should count as equal, direct slice equality is too strict. You need a callback that implements set-style semantics, or you need to canonicalize the values before calling maps.EqualFunc.
The helper does not define domain equality for you. It only provides the map-level structure for applying the rule consistently to matching keys.
Keep normalization rules explicit
Custom equality often appears when data has a canonical form. Text might be case-insensitive, timestamps might be reduced to a chosen precision, or a struct may contain fields that do not participate in semantic equality.
Put those decisions where a reviewer can see them:
same := maps.EqualFunc(cached, incoming, func(a, b Record) bool {
return a.ID == b.ID &&
strings.EqualFold(a.Region, b.Region) &&
a.Status == b.Status
})This is clearer than comparing serialized forms or stripping fields through several temporary transformations. The callback shows exactly which fields matter.
Be careful with tolerant numeric comparisons. A rule such as math.Abs(a-b) < epsilon can be valid for a specific numerical domain, but the tolerance has to come from that domain’s requirements. A generic epsilon copied into unrelated code can make distinct values compare equal without a sound basis.
Nil and empty maps compare as equal when both have no entries
For map equality, entries are what matter. A nil map and an allocated empty map both contain zero entries, so maps.EqualFunc reports them as equal:
var left map[string][]int
right := map[string][]int{}
same := maps.EqualFunc(left, right, func(a, b []int) bool {
return slices.Equal(a, b)
})
fmt.Println(same) // trueThis behavior is convenient when nil and empty both mean “no values” in your application. If the distinction itself carries meaning, maps.EqualFunc is not sufficient on its own. Check nil state separately before comparing entries.
The same principle applies to other representation details that aren’t visible through key-value equality. Decide whether those details belong to the application’s equality contract before choosing the helper.
A matching value rule cannot compensate for different keys
A common mistake is treating maps.EqualFunc as a general matching engine. It isn’t. The function compares values associated with identical keys.
Consider these maps:
left := map[string]string{
"service-a": "ready",
}
right := map[string]string{
"SERVICE-A": "READY",
}A case-insensitive value callback can make "ready" and "READY" equivalent, but the keys still differ. The result is false.
If keys need canonicalization, do that before comparison. For example, a parser might normalize identifiers to lowercase as data enters the system. Once both maps follow the same key convention, maps.EqualFunc can focus on value semantics.
Also avoid callbacks with side effects. Equality checks are easier to reason about when the callback only examines its two arguments. Updating counters, modifying referenced data, or depending on call order turns a simple predicate into behavior that is harder to test and maintain.
Choose maps.EqualFunc for semantic value equality
Use maps.EqualFunc when map structure matters exactly but value equality needs a domain-specific rule. It supports different value types and values that cannot use ==, while preserving normal equality for keys.
If both maps have comparable values and ordinary == semantics are correct, maps.Equal is simpler. If keys also need fuzzy or normalized matching, transform the representation first rather than stretching a value callback beyond its role.
A good next step is to write the equality rule as a small named function when it appears in more than one call site. That gives the application one visible definition of equivalent values and keeps map comparisons consistent.