Maps are one of Go's most useful built-in data structures. A Go map stores key-value pairs and provides fast lookup when you know the key associated with a value.

If you've worked with a Java HashMap, a Python dictionary or a JavaScript Map, the basic idea will be familiar. In Go, the syntax is compact and map support is built directly into the language.

What is a map in Go?

A Go map associates a set of unique keys with values. Both the key type and value type are declared when the map is created.

For example, this map uses integers as keys and strings as values:

people := make(map[int]string)

people[1] = "Walker"
people[2] = "Cameron"

The type of this map is map[int]string. That means every key must be an int and every value must be a string.

Go also supports map literals, which are often more convenient when the initial data is already known:

people := map[int]string{
    1: "Walker",
    2: "Cameron",
}

Create and initialize a Go map

Here's a complete example that maps programming language names to an admittedly subjective difficulty rating:

package main

import "fmt"

func main() {
    languages := map[string]string{
        "Go":     "Easy",
        "Python": "Easy",
        "Scala":  "Difficult",
        "Java":   "Moderately difficult",
        "C++":    "Difficult",
    }

    for language, difficulty := range languages {
        fmt.Println(language, difficulty)
    }
}

The range keyword returns both the key and value on each iteration, so there is no need to retrieve the value from the map separately.

Go map iteration order

One important characteristic of Go maps is that their iteration order is not specified.

If you run the previous example several times, the entries might appear in different orders. This is intentional behavior. A program must not depend on a Go map returning its keys in insertion order, alphabetical order or any other predictable order.

If the order matters, collect the keys and sort them explicitly.

How to sort a Go map

You do not sort a map itself. Instead, extract the keys into a slice, sort the slice and then use those sorted keys to access the map.

Modern Go includes the generic slices package, which makes this straightforward:

package main

import (
    "fmt"
    "slices"
)

func main() {
    languages := map[string]string{
        "Go":     "Easy",
        "Python": "Easy",
        "Scala":  "Difficult",
        "Java":   "Moderately difficult",
        "C++":    "Difficult",
    }

    keys := make([]string, 0, len(languages))

    for key := range languages {
        keys = append(keys, key)
    }

    slices.Sort(keys)

    for _, key := range keys {
        fmt.Printf("%-6s %s\n", key, languages[key])
    }
}

The result is now deterministic and alphabetically sorted:

C++    Difficult
Go     Easy
Java   Moderately difficult
Python Easy
Scala  Difficult

The important distinction is that slices.Sort sorts the slice of keys. It does not change the internal ordering of the map itself.

Retrieve a value from a Go map

Retrieving a value is as simple as placing the key inside square brackets:

languages := map[string]string{
    "Go":   "Easy",
    "Java": "Moderately difficult",
}

difficulty := languages["Go"]

fmt.Println(difficulty)

This prints:

Easy

Test whether a map contains a key

There's one detail developers must understand when retrieving map values. If a key does not exist, Go returns the zero value for the map's value type.

For a map[string]string, that means a missing key returns an empty string. That creates an ambiguity if an empty string is itself a valid map value.

Go solves this with the comma-ok idiom:

difficulty, ok := languages["Go"]

if ok {
    fmt.Println("Go:", difficulty)
} else {
    fmt.Println("Go was not found")
}

The second return value is a Boolean. It is true when the key exists and false when it does not.

If you only care whether the key exists and do not need its value, use the blank identifier:

if _, ok := languages["Rust"]; !ok {
    fmt.Println("Rust was not found")
}

Add and update Go map values

The same syntax is used to add a new entry or update an existing one:

languages["Rust"] = "Moderate"
languages["Java"] = "Easy"

If Rust does not exist, Go adds it. If Java already exists, Go replaces its current value.

Delete an entry from a Go map

Go's built-in delete function removes a key and its associated value:

delete(languages, "Scala")

Calling delete for a key that does not exist is safe and does not produce an error.

Get the size of a Go map

The built-in len function returns the number of key-value pairs currently stored in a map:

fmt.Println(len(languages))

Use structs as Go map values

Map values do not have to be simple types such as strings or integers. They can also be structs.

For example, a student ID can serve as the map key while a Student struct stores information about the student:

package main

import "fmt"

type Student struct {
    Name   string
    Course string
    Grade  string
}

func main() {
    students := map[int]Student{
        1: {
            Name:   "Walker",
            Course: "Calculus",
            Grade:  "A",
        },
        2: {
            Name:   "Cameron",
            Course: "Computer Science",
            Grade:  "A",
        },
    }

    for id, student := range students {
        fmt.Printf(
            "ID: %d, Name: %s, Course: %s, Grade: %s\n",
            id,
            student.Name,
            student.Course,
            student.Grade,
        )
    }
}

This pattern is particularly useful when a unique identifier, username, product code or some other value provides a natural key for looking up structured data.

Which Go types can be map keys?

Not every Go type can be used as a map key. A map key must support equality comparisons with == and !=.

Common map key types include:

  • strings;
  • integers and other numeric types;
  • Booleans;
  • pointers;
  • arrays whose elements are comparable; and
  • structs whose fields are all comparable.

Slices, maps and functions cannot be used as map keys because those types are not comparable.

Nil maps vs. empty maps

There's another map behavior worth knowing. A declared but uninitialized map has a value of nil:

var languages map[string]string

You can safely read from a nil map, but attempting to add an entry causes a runtime panic.

var languages map[string]string

fmt.Println(languages["Go"]) // Safe
languages["Go"] = "Easy"    // Runtime panic

Initialize the map with make or a map literal before adding values:

languages := make(map[string]string)
languages["Go"] = "Easy"

Go map cheat sheet

// Create an empty map
languages := make(map[string]string)

// Add a value
languages["Go"] = "Easy"

// Retrieve a value
difficulty := languages["Go"]

// Check whether a key exists
difficulty, ok := languages["Go"]

// Delete an entry
delete(languages, "Go")

// Count entries
size := len(languages)

// Iterate over keys and values
for key, value := range languages {
    fmt.Println(key, value)
}

Go maps provide a compact and efficient way to associate keys with values. Once you understand map literals, range, the comma-ok idiom, delete and the fact that iteration order is unspecified, you have most of the map functionality needed for everyday Go development.

Top Joe Rogan JRE podcasts for programmers and developers