Free Ebook cover Go (Golang) Fundamentals: Simple, Fast Programs for Beginners

Go (Golang) Fundamentals: Simple, Fast Programs for Beginners

New course

12 pages

Maps in Go: Key-Value Data, Lookups, and Idiomatic Patterns

Capítulo 6

Estimated reading time: 8 minutes

+ Exercise

What a map is (and when to use it)

A map stores key-value pairs, letting you look up a value by its key efficiently. Use maps when you need fast membership tests ("have we seen this?"), counting, grouping, indexing by an ID, or building configuration tables. In Go, maps are reference types: the map variable holds a header pointing to runtime-managed data.

1) Creating maps: literals, make, and nil behavior

Map literals

Use a literal when you know initial entries at compile time. This is readable and common in production code for small tables.

package main

import "fmt"

func main() {
	ports := map[string]int{
		"http":  80,
		"https": 443,
	}

	fmt.Println(ports["https"]) // 443
}

Creating an empty map with make

Use make when you want an empty map you can write to. Optionally provide an initial capacity hint (not a limit) to reduce allocations when you expect many entries.

usersByID := make(map[int]string)
counts := make(map[string]int, 1000) // capacity hint

Nil maps: readable, but not writable

A nil map is the zero value of a map type. Reading from a nil map is safe and returns the zero value for the value type. Writing to a nil map panics.

package main

import "fmt"

func main() {
	var m map[string]int // nil

	fmt.Println(m["missing"]) // 0 (safe)

	// m["x"] = 1 // panic: assignment to entry in nil map
}

When to initialize

  • Initialize with make when you will insert/update entries.

    Continue in our app.

    You can listen to the audiobook with the screen off, receive a free certificate for this course, and also have access to 5,000 other free online courses.

    Or continue reading below...
    Download App

    Download the app

  • Leave as nil when a map is optional and you only need reads, or you want to signal “not set” without allocating. Many APIs treat nil maps as empty for reads.

  • Initialize at the boundary: if a function will write to a map passed in, either document that it must be non-nil or allocate it inside and return it.

func ensureMap(m map[string]int) map[string]int {
	if m == nil {
		m = make(map[string]int)
	}
	return m
}

2) Insert, update, delete, and lookup with the “comma ok” idiom

Insert and update use the same syntax

Assigning to a key inserts it if missing, or overwrites it if present.

scores := make(map[string]int)

scores["alice"] = 10 // insert
scores["alice"] = 11 // update

Delete

delete removes a key if present. Deleting a missing key is a no-op (safe).

delete(scores, "alice")
delete(scores, "missing") // safe

Lookup: distinguish “missing” from “present with zero value”

Indexing a map returns the value type’s zero value when the key is absent. If the value type can naturally be zero (like 0, "", false), you often need to know whether the key existed. Use the “comma ok” idiom.

package main

import "fmt"

func main() {
	counts := map[string]int{"go": 0}

	v := counts["missing"]
	fmt.Println(v) // 0, but key is missing

	if v, ok := counts["go"]; ok {
		fmt.Println("go present with value", v)
	}

	if v, ok := counts["missing"]; !ok {
		fmt.Println("missing key; v is", v)
	}
}

Common pitfall: using a map lookup directly in conditionals

Because missing keys yield a zero value, code like if counts[word] > 0 can be misleading if you need to distinguish “never seen” from “seen but count is 0”. Prefer comma ok when existence matters.

// Existence check
if _, ok := counts[word]; ok {
	// key exists
}

3) Iteration: range order is not guaranteed; sorting keys for deterministic output

Map iteration order is intentionally randomized

When you iterate over a map with range, the order is not specified and can change between runs. Do not rely on it for stable output, tests, or user-facing formatting.

for k, v := range scores {
	fmt.Println(k, v)
}

Deterministic iteration: collect keys, sort, then index

For stable output (logs, CLI output, tests), sort the keys. This is the idiomatic pattern.

package main

import (
	"fmt"
	"sort"
)

func main() {
	ports := map[string]int{"https": 443, "http": 80, "ssh": 22}

	keys := make([]string, 0, len(ports))
	for k := range ports {
		keys = append(keys, k)
	}
	sort.Strings(keys)

	for _, k := range keys {
		fmt.Printf("%s => %d\n", k, ports[k])
	}
}

If your keys are integers, use sort.Ints. For custom ordering, use sort.Slice.

4) Map value types: pointers vs structs, and updating nested values

Why value type choice matters

Maps return a copy of the stored value when the value type is a struct. That means you cannot modify struct fields directly through the map index expression. This is a frequent beginner pitfall and also a design choice you should make consciously in production code.

Pitfall: cannot assign to a field of a struct stored in a map

type User struct {
	Name  string
	Score int
}

func main() {
	users := map[string]User{
		"alice": {Name: "Alice", Score: 10},
	}

	// users["alice"].Score = 11 // compile error
}

Pattern A (common): read-modify-write the struct

Get the value, modify the local copy, then store it back.

u := users["alice"]
u.Score++
users["alice"] = u

Combine with comma ok if the key might be missing.

if u, ok := users["bob"]; ok {
	u.Score += 5
	users["bob"] = u
}

Pattern B: store pointers when you need in-place updates

If you frequently update fields, consider map[string]*User. Then you can mutate through the pointer. Be mindful of nil pointers and shared mutation.

users := map[string]*User{
	"alice": {Name: "Alice", Score: 10},
}

users["alice"].Score++

When inserting, allocate a new struct and store its address.

users["bob"] = &User{Name: "Bob", Score: 0}

Nested updates: map values that contain maps

When a struct contains a map field, you still need to ensure the inner map is initialized before writing to it.

type Profile struct {
	Tags map[string]bool
}

profiles := make(map[string]Profile)

p := profiles["alice"]
if p.Tags == nil {
	p.Tags = make(map[string]bool)
}
p.Tags["golang"] = true
profiles["alice"] = p

If you use pointers, the nested update can be simpler, but you still must initialize the inner map.

profiles := map[string]*Profile{"alice": {}}

if profiles["alice"].Tags == nil {
	profiles["alice"].Tags = make(map[string]bool)
}
profiles["alice"].Tags["golang"] = true

5) Concurrency note: maps are not safe for concurrent writes

Go’s built-in maps are not safe for concurrent writes. If one goroutine writes while another reads or writes without synchronization, your program can crash with a runtime error or exhibit data races.

  • Use a sync.Mutex to guard a normal map when you need shared mutable state.

  • Use sync.RWMutex when you have many readers and fewer writers.

  • Consider sync.Map for specific patterns (highly concurrent, mostly reads, or keys that live for the program lifetime). It has different tradeoffs and API.

  • Prefer message passing: keep the map owned by one goroutine and communicate via channels when appropriate.

// Sketch: mutex-protected map (pattern preview)

type SafeCounts struct {
	mu sync.Mutex
	m  map[string]int
}

func (s *SafeCounts) Inc(key string) {
	s.mu.Lock()
	defer s.mu.Unlock()
	if s.m == nil {
		s.m = make(map[string]int)
	}
	s.m[key]++
}

Exercises

Exercise 1: Word frequency counter

Goal: Given a slice of words, build a map of word => count, then print results in deterministic (sorted) order.

Steps:

  • Create counts := make(map[string]int).

  • Loop over the words and increment: counts[w]++.

  • Collect keys into a slice, sort it, then print key and counts[key].

package main

import (
	"fmt"
	"sort"
	"strings"
)

func main() {
	text := "go is fun and go is fast"
	words := strings.Fields(text)

	counts := make(map[string]int)
	for _, w := range words {
		counts[w]++
	}

	keys := make([]string, 0, len(counts))
	for k := range counts {
		keys = append(keys, k)
	}
	sort.Strings(keys)

	for _, k := range keys {
		fmt.Printf("%s: %d\n", k, counts[k])
	}
}

Exercise 2: Configuration map with validation using comma ok

Goal: Validate required configuration keys and parse values. Use comma ok to produce clear error messages when keys are missing.

Scenario: You receive configuration as map[string]string with required keys: "host", "port", "env". Optional key: "debug" (defaults to false).

Steps:

  • Write a function LoadConfig(cfg map[string]string) (Config, error).

  • For each required key, use v, ok := cfg["key"]; if !ok return an error that names the missing key.

  • Convert port to an integer; validate range (1–65535).

  • For optional debug, if missing, keep default; if present, parse true/false.

package main

import (
	"fmt"
	"strconv"
)

type Config struct {
	Host  string
	Port  int
	Env   string
	Debug bool
}

func LoadConfig(cfg map[string]string) (Config, error) {
	var c Config

	host, ok := cfg["host"]
	if !ok || host == "" {
		return c, fmt.Errorf("missing required config: host")
	}
	c.Host = host

	portStr, ok := cfg["port"]
	if !ok || portStr == "" {
		return c, fmt.Errorf("missing required config: port")
	}
	port, err := strconv.Atoi(portStr)
	if err != nil {
		return c, fmt.Errorf("invalid port %q: %w", portStr, err)
	}
	if port < 1 || port > 65535 {
		return c, fmt.Errorf("port out of range: %d", port)
	}
	c.Port = port

	env, ok := cfg["env"]
	if !ok || env == "" {
		return c, fmt.Errorf("missing required config: env")
	}
	c.Env = env

	if debugStr, ok := cfg["debug"]; ok && debugStr != "" {
		b, err := strconv.ParseBool(debugStr)
		if err != nil {
			return c, fmt.Errorf("invalid debug %q: %w", debugStr, err)
		}
		c.Debug = b
	}

	return c, nil
}

func main() {
	cfg := map[string]string{
		"host":  "localhost",
		"port":  "8080",
		"env":   "dev",
		"debug": "true",
	}

	c, err := LoadConfig(cfg)
	if err != nil {
		panic(err)
	}
	fmt.Printf("%+v\n", c)
}

Now answer the exercise about the content:

In Go, why is the “comma ok” idiom useful when reading from a map like map[string]int?

You are right! Congratulations, now go to the next page

You missed! Try again.

Map indexing returns the value type’s zero value when a key is absent. The “comma ok” form returns a boolean so you can tell whether the key exists, even when the value could legitimately be zero.

Next chapter

Structs and Methods in Go: Modeling Data and Behavior

Arrow Right Icon
Download the app to earn free Certification and listen to the courses in the background, even with the screen off.