Why You Can't Take the Address of a Map Value in Go

Map lookups in Go return value copies, not references, so you must use pointer types or reassign values to modify map data.

Why map values have no address

You write a map lookup, try to pass the address of the result to a function, and the compiler rejects the code. Or you attempt to increment a map value in place and find the change disappears. This happens because Go maps do not store values in fixed memory slots you can point to. Map lookups return a copy of the value, not a reference to the stored data. The language forbids taking the address of a map element to prevent dangling pointers caused by internal map resizing.

The hotel room analogy

Think of a map like a hotel where rooms get rearranged constantly. The hotel manager moves guests to different rooms whenever the building needs to expand or optimize space. If you write down the room number of a guest, that number might be valid for a second, then point to an empty wall or a stranger the next moment.

Go refuses to let you write down the room number. When you ask for a guest's details, the front desk gives you a photocopy of their file. You can read the copy, modify the copy, and hand the copy back to update the record. But you cannot get a permanent pointer to the file inside the desk because the desk might shuffle the files at any time.

If you need to track a value across moves, you store a badge that travels with the guest. In Go, that badge is a pointer. You store the pointer inside the map, and the pointer points to data on the heap that the map does not move.

Maps are dynamic. Addresses are static. Go keeps them separate.

Minimal example

Here's the compile error you hit when you try to address a map value directly. The compiler blocks the operation before the program runs.

package main

import "fmt"

func main() {
    // Map stores int values. Access returns a copy.
    scores := map[string]int{"alice": 10}

    // Compiler rejects this line.
    // Error: cannot take the address of map element scores["alice"]
    // addr := &scores["alice"]

    // Store a pointer to get a stable address.
    // The map holds *int. The int lives on the heap.
    ptrScores := map[string]*int{"alice": new(int)}
    *ptrScores["alice"] = 10

    // Lookup returns *int. We can take the address of the pointer.
    // This points to the map slot, not the int.
    addr := &ptrScores["alice"]
    fmt.Println(addr)
}

How map lookups work

When you write m["key"], Go performs a hash lookup. It computes the hash of the key, finds the bucket in the internal hash table, and copies the value to a temporary location. That temporary location is what you receive. Taking the address of a temporary copy is useless because the copy vanishes at the end of the statement.

The deeper reason involves map resizing. Go maps are hash tables with buckets. When the map grows beyond a certain load factor, the runtime allocates a new, larger set of buckets and moves entries over. This resize can happen during any map operation, including reads and writes. If Go allowed &m["key"], your pointer would reference the old bucket. After a resize, that pointer would dangle, pointing to memory that might be freed or reused.

The compiler prevents this by making map elements non-addressable. The rule is hard. You cannot work around it with type assertions, interfaces, or unsafe tricks in standard code. The compiler sees the map index expression and blocks the address-of operator.

The compiler blocks the address to protect you from the map's internal movement.

Realistic example: updating structs

Real code often needs to mutate state. If you have a map of structs, you cannot pass a field to a function that takes a pointer. The lookup returns a struct value, and you cannot take its address. The solution is to store pointers in the map.

package main

import "fmt"

type Config struct {
    Timeout int
    Debug   bool
}

// ApplyDefaults modifies a config in place.
// It requires a pointer to the struct.
func ApplyDefaults(c *Config) {
    if c.Timeout == 0 {
        c.Timeout = 30
    }
}

func main() {
    // Map of structs. Values are copied.
    configs := map[string]Config{
        "api": {Timeout: 0, Debug: true},
    }

    // This fails. ApplyDefaults needs *Config.
    // configs["api"] returns Config, not *Config.
    // ApplyDefaults(&configs["api"]) // Compile error.

    // Map of pointers solves this.
    ptrConfigs := map[string]*Config{
        "api": {Timeout: 0, Debug: true},
    }

    // Lookup returns *Config. Pass it directly.
    ApplyDefaults(ptrConfigs["api"])
    fmt.Println(ptrConfigs["api"].Timeout) // Prints: 30
}

The reassignment pattern

You do not always need pointers. If you only need to read, modify, and write back, the reassignment pattern works with value maps. This avoids pointer overhead and nil checks.

package main

import "fmt"

func main() {
    // Map of ints. No pointers needed for simple updates.
    counts := map[string]int{"visits": 100}

    // Read the value into a local variable.
    // This creates a copy you can modify freely.
    visits := counts["visits"]

    // Modify the local copy.
    visits++

    // Write the updated value back to the map.
    counts["visits"] = visits

    fmt.Println(counts["visits"]) // Prints: 101
}

This pattern is safe and idiomatic for small values. It makes the update explicit. The map is not modified until the final assignment.

Pitfalls and edge cases

The compiler error is straightforward: cannot take the address of map element. You cannot suppress it. Even if the map value is a pointer, the map element itself is not addressable.

Consider map[string]*int. The element is a *int. You still cannot write &m["key"]. The compiler rejects this with the same error. The map slot holds the pointer, but you cannot take the address of the slot. If you need the address of the pointer, assign it to a variable first.

p := m["key"]
addr := &p // Valid. p is a local variable.

This distinction trips up developers who assume pointer values bypass the rule. The rule applies to the map element, regardless of the element's type.

Pointer maps introduce nil risks. m["missing"] returns the zero value for the map's value type. For map[string]*T, the zero value is nil. Dereferencing a nil pointer causes a runtime panic: invalid memory address or nil pointer dereference. Always check for nil or use the comma-ok idiom.

if val, ok := ptrMap["key"]; ok {
    // val is *T. Check if val is nil if the map allows nil values.
    if val != nil {
        fmt.Println(*val)
    }
}

Convention aside: Go developers often prefer if val, ok := m["key"]; ok over checking nil separately. This pattern handles missing keys cleanly. If your map never stores nil pointers, you can skip the nil check after the ok guard.

Pointers in maps also add memory overhead. Each pointer adds indirection and consumes a word of memory on the map entry plus the heap allocation. For small values like int or bool, map[K]V is faster and smaller than map[K]*V. Use pointers only when you need mutation or sharing.

Pointers in maps shift the burden of nil checks to you. Verify before dereferencing.

Decision: map types and pointers

Choose the map type based on how you access and update data.

Use map[K]V when values are small, immutable, or you only need to read and reassign. This is the default choice. It avoids pointer overhead and nil panics. The reassignment pattern handles updates safely.

Use map[K]*V when you need to pass map values to functions that take pointers, or when you need to mutate values in place without reassigning. This is common for maps of structs where multiple functions modify fields. It also helps when you need to share a value across multiple data structures.

Use a local copy when you need to modify a value temporarily without updating the map immediately. Read the value, work on the copy, and write it back only if the operation succeeds. This keeps the map consistent during partial updates.

Use a slice of structs with a lookup map when you need to iterate efficiently and avoid pointer indirection overhead. Store structs in a slice and use map[K]int to index into the slice. This combines fast iteration with fast lookups while keeping values addressable via slice indices.

Pointers enable mutation. Values enable simplicity. Choose based on how you update data.

Where to go next