How to Use a Map with Struct Keys in Go

Use structs as Go map keys only if all fields are comparable types, or implement custom hashing for complex cases.

How to Use a Map with Struct Keys in Go

You are building a grid-based game. You want to cache the terrain type for every coordinate. You define a Point struct with X and Y integers. You create a map[Point]string and start populating it. It works perfectly. Then you decide to add a VisitedBy []string field to track which players have stepped on each tile. You save the file. The compiler rejects the build with invalid map key type Point. You haven't changed the map logic. You just added a slice. The compiler is enforcing a rule about how Go compares values, and that rule just got broken.

Go maps rely on two operations: hashing and equality. When you insert a key, Go computes a hash value to decide which bucket the entry goes into. When you look up a key, Go computes the hash again, finds the bucket, and then checks every entry in that bucket for equality. If the key type supports both operations, Go can use it. Structs support these operations only if every single field inside them supports them. This is called comparability. A struct is comparable if all its fields are comparable. Integers, strings, booleans, pointers, and channels are comparable. Slices, maps, and functions are not. If a struct contains a slice, the struct itself becomes incomparable. You cannot compare two slices for equality in Go, so Go refuses to compare two structs that contain slices.

Structs are comparable only if their fields are. The compiler enforces this at build time.

Minimal example

Here is the simplest case that works, followed by the moment it breaks.

package main

import "fmt"

// Point holds coordinates. All fields are ints, which are comparable.
type Point struct {
    X int
    Y int
}

// BrokenPoint adds a slice. Slices are not comparable.
type BrokenPoint struct {
    X       int
    Y       int
    History []string // slices cannot be compared for equality
}

func main() {
    // This compiles. Point is fully comparable.
    terrain := make(map[Point]string)
    terrain[Point{1, 2}] = "forest"

    // This fails to compile. BrokenPoint contains a slice.
    // The compiler rejects this with: invalid map key type BrokenPoint
    // badMap := make(map[BrokenPoint]string)

    fmt.Println(terrain[Point{1, 2}])
}

Why slices are forbidden

The restriction on slices exists to prevent ambiguous behavior and performance traps. A slice variable is not the data itself. It is a header containing a pointer to an underlying array, a length, and a capacity. If Go allowed slices as map keys, it would have to decide what "equality" means. Comparing the header checks if two slices point to the same memory address. That is rarely useful for a map key. You usually want to know if the contents are the same.

Comparing contents requires checking the length and then looping through every element. This turns a map lookup from a fast hash operation into a slow linear scan. The cost grows with the size of the slice. Maps are designed for O(1) lookups. Allowing slices would degrade performance unpredictably.

Worse, slices can alias. Two different slice variables can point to the same underlying array. If you use a slice as a key and then modify the underlying array through one of the slices, the key inside the map changes silently. The entry becomes unreachable. The map leaks memory. Go prevents this entire class of bugs by banning slices as keys. The same logic applies to maps and functions. They are reference types with internal state that can change. Go requires map keys to be immutable in terms of their identity.

Nested structs inherit these rules. If a struct contains another struct, the inner struct must also be comparable. The compiler walks the entire type tree.

// Address contains a slice, making it incomparable.
type Address struct {
    Street string
    Tags   []string
}

// User contains Address. The entire struct is incomparable.
type User struct {
    Name    string
    Address Address
}

// This fails. User contains Address, which contains a slice.
// The compiler rejects this with: invalid map key type User
// userMap := make(map[User]int)

Realistic example: string keys

When a struct contains slices or maps, you cannot use it directly as a key. The standard workaround is to generate a comparable key from the struct. A string key is the most common approach. You write a method that formats the struct fields into a deterministic string. Use that string as the map key.

The receiver name follows Go convention: one or two letters matching the type. Here c matches Config. The method returns a value, so it does not modify the struct.

package main

import (
    "fmt"
    "strings"
)

// Config contains a slice, making it incomparable.
type Config struct {
    Region  string
    Tags    []string
    Timeout int
}

// Key returns a deterministic string for map lookups.
// The receiver is a value copy, so the original struct is not modified.
func (c Config) Key() string {
    // Sort tags to ensure ["a","b"] and ["b","a"] produce the same key.
    // In production code, use slices.Sort here.
    return fmt.Sprintf("%s:%s:%d", c.Region, strings.Join(c.Tags, ","), c.Timeout)
}

The map uses the string key. Lookup requires regenerating the key.

func main() {
    // Map uses the string key derived from the struct.
    cache := make(map[string]Config)

    cfg := Config{Region: "us-east", Tags: []string{"web"}, Timeout: 30}
    cache[cfg.Key()] = cfg

    // Lookup regenerates the key to find the entry.
    if val, ok := cache[cfg.Key()]; ok {
        fmt.Println("Cached", val.Region)
    }
}

Generate keys deterministically. Sort slices before formatting.

Pitfalls and errors

The compiler rejects incomparable keys with invalid map key type StructName. This error is immediate. You cannot work around it at runtime. If you try to use a struct with a slice as a key, the build fails. This is a safety feature. It forces you to think about how you represent equality.

A common trap is non-deterministic keys. If you convert a struct to a string key using fmt.Sprintf, the order of fields in a slice matters. A config with tags ["a", "b"] produces a different key than ["b", "a"]. If logical equality requires those to be the same, you must sort the slice before generating the key. Failing to sort leads to cache misses. The map will contain two entries for what you consider the same config.

Performance is another concern. Generating a string key allocates memory and formats data. For hot paths, this allocation adds up. If you are caching in a tight loop, the allocation pressure can trigger garbage collection pauses. If you need high performance, consider a separate ID field or a custom hash strategy.

Never use json.Marshal for map keys. JSON output order for maps is not guaranteed. Two identical structs might produce different JSON strings if they contain maps. Use fmt.Sprintf with a fixed field order or a custom method. The key must be stable across runs and goroutines.

Go maps do not support custom hash functions. You cannot implement a Hash() method to make a struct a map key. The language design keeps maps simple and fast by restricting keys to comparable types. If you need custom hashing, you must manage the data structure yourself or use a third-party library.

The compiler error is a feature. It prevents ambiguous comparisons.

Decision matrix

Use a struct as a map key when every field is a comparable type like int, string, bool, or pointer. This gives you type safety and zero overhead for key generation.

Use a string key derived from the struct when the struct contains slices or maps and you still need map semantics. Generate the string with a deterministic format to ensure unique keys for unique states.

Use a separate ID field when the struct is large or complex. Add a ID string or ID int field to the struct and use that as the key. This keeps the key small and the value flexible.

Use a slice of structs when you need to store items with slices and don't require fast lookups. Iterate over the slice to find matches. This avoids key generation overhead and comparability restrictions.

Use maphash with a custom key type when you need high-performance hashing for complex data. Implement a custom type with a Hash() method and manage the map buckets manually or use a third-party library.

Pick the key strategy that matches your data. Don't force a map where a slice works.

Where to go next