Map Type and Declaration

How to declare map variables in Go, the map type syntax, key comparability rules, and the behaviour of a nil map

A map in Go is a built-in type that stores key-value pairs. Under the hood it is implemented as a hash table, which gives you fast lookups, inserts, and deletes. Before you can work with a map you first need to understand its type notation and what a declared-but-uninitialized map actually is.

The map type syntax

Every map variable has a type written as map[KeyType]ValueType. KeyType is the type of the keys you will use to look up entries, and ValueType is the type of the values stored under each key.

A bare declaration that spells out that type, without any initialisation, looks like this:

var scores map[string]int

After that line, scores is a map variable of type map[string]int. The keys are strings and the values are integers. But the variable does not yet point to an actual hash table — its value is nil.

Reference type:

Map types are reference types, just like slices and pointers. The variable holds a reference to the underlying data structure; it does not hold the data itself. Copying a map variable copies the reference, not the entries.

What a nil map can and cannot do

A nil map reads like an empty map — fetching a key returns the zero value of the value type and never panics. Writing to a nil map, however, causes a runtime panic.

var counts map[string]int  // nil map
fmt.Println(counts["missing"]) // prints 0, no panic
counts["new"] = 1               // panic: assignment to entry in nil map

This example matters because the panic only surfaces when the program actually runs, not at compile time. The compiler will not warn you about it. If you come from a language where maps are automatically allocated, this behaviour can catch you off guard.

Writing to nil map panics:

Any assignment to a key on a nil map will stop your program with panic: assignment to entry in nil map. You must initialise the map first — either with make or with a map literal — before storing values.

What counts as a valid key type

The Go specification requires map keys to be comparable — meaning you can use the == and != operators on them. If a type cannot be compared with ==, you cannot use it as a map key. The compiler enforces this at declaration time, so you get a clear error right away.

Types that are comparable and can be used as keys:

  • Booleans
  • Numeric types (int, float64, etc.)
  • Strings
  • Pointers
  • Channels
  • Interfaces (as long as the underlying dynamic type is comparable — a panic can still happen at runtime if a stored interface value turns out to be non-comparable)
  • Structs whose fields are all comparable
  • Arrays whose element type is comparable

Types that are not comparable, and therefore cannot be map keys:

  • Slices
  • Maps
  • Functions
  • Structs or arrays that contain any of the above
// Valid key types
var m1 map[int]string
var m2 map[[3]int]bool        // array of ints — comparable
var m3 map[struct{ X, Y float64 }]string
// Invalid key types — will not compile
var bad1 map[[]byte]int        // slice key
var bad2 map[map[string]bool]int // map key

Attempting to declare a map with a non-comparable key type gives a compilation error like invalid map key type .... This is deliberate and protects you from undefined behaviour that would occur in a hash table if equality were not well-defined.

Interface keys can panic at runtime:

Although interface types themselves are comparable, a value stored as an interface key might hold a non-comparable concrete type. If that happens, a runtime panic occurs when the map tries to compare keys during a lookup or insert. If you use interface keys, make sure you only store comparable dynamic values.

Structs as keys — a practical pattern

Struct keys let you index by multiple dimensions in a single map, rather than nesting maps inside maps. This becomes useful when you need to tally data by more than one attribute.

type Key struct {
    Path    string
    Country string
}
var hits map[Key]int

Because Key contains only two string fields, both of which are comparable, the whole struct is comparable and can be a map key. Incrementing a counter for a Vietnamese visitor to the home page is then straightforward — once the map is initialised, of course:

hits = make(map[Key]int)
hits[Key{"/", "vn"}]++

Without struct keys you would need a map[string]map[string]int and extra checks to see if the inner map exists before writing to it. A struct key collapses that into one level, which keeps the code shorter and clearer.

Correct declaration compiles cleanly:

If your map[KeyType]ValueType compiles without errors, the key type meets the comparability requirement. This gives you confidence that the map will behave predictably — once initialised.

The connection between type and initialisation

The map type is separate from the map value. When you write var m map[string]int, you have described what kind of map you want, but you have not yet created the internal hash table. The type declaration is the blueprint; make and map literals build the actual structure from it.