Eine Map ist Gos eingebaute Hashmap — eine ungeordnete Sammlung von Key-Value-Paaren mit konstantem Zugriff im Mittel. Wie Slice ist Map ein Referenz-Typ: Du arbeitest in Wahrheit mit einem schmalen Header, der auf die eigentliche Hashtabelle zeigt. Das macht Maps schnell und billig zu übergeben — eröffnet aber auch ein paar harte Kanten: Schreiben in eine nil-Map paniciert, die Iterations-Reihenfolge ist absichtlich randomisiert, und nebenläufiges Schreiben erkennt die Runtime aktiv und bricht das Programm ab. Dieser Artikel zeigt, wie Maps in Go entstehen, wie das comma-ok-Idiom existenz-sichere Lookups erlaubt, was beim range zu beachten ist und welche Helfer das ab Go 1.21 verfügbare maps-Paket mitbringt.

Was eine Map in Go ist

Die Sprach-Spezifikation definiert Map kurz und präzise:

A map is an unordered group of elements of one type, called the element type, indexed by a set of unique keys of another type called the key type.

Die wichtigsten Eigenschaften:

  • Hashmap unter der Haube — Lookup, Insert und Delete sind im Mittel in konstanter Zeit O(1).
  • Ungeordnet — es gibt kein „erstes” oder „letztes” Element. Reihenfolge beim range ist randomisiert.
  • Referenz-Typ — wie Slice oder Channel: ein Map-Wert ist effektiv ein Pointer auf eine interne Datenstruktur. Kopien teilen sich die Daten.
  • Eindeutige Keys — pro Key gibt es maximal einen Wert. Erneute Zuweisung überschreibt.
  • Typisiert — Key-Typ und Wert-Typ stehen zur Compile-Zeit fest.

Die Syntax für den Typ ist map[KeyType]ValueType:

Go map_basics.go
package main

import "fmt"

func main() {
    // Eine Map von Land-Code zu Hauptstadt
    capitals := map[string]string{
        "DE": "Berlin",
        "AT": "Wien",
        "CH": "Bern",
    }

    fmt.Println(capitals["DE"])
    fmt.Println("Anzahl Länder:", len(capitals))
}
Output
Berlin
Anzahl Länder: 3

Map erstellen — make, Literal oder nil

Es gibt drei Wege, eine Map ins Leben zu rufen — mit unterschiedlichen Konsequenzen:

Go map_create.go
package main

import "fmt"

func main() {
    // 1) nil-Map — deklariert, aber nicht initialisiert
    var a map[string]int
    fmt.Println("a == nil?", a == nil, "len:", len(a))

    // Lesen aus nil-Map ist OK — gibt Zero-Value zurück
    fmt.Println("a[\"x\"]:", a["x"])

    // a["x"] = 1   // <-- würde paniken: assignment to entry in nil map

    // 2) make() — leere, beschreibbare Map
    b := make(map[string]int)
    b["x"] = 1

    // 3) Map-Literal — initialisiert mit Werten
    c := map[string]int{
        "x": 1,
        "y": 2,
    }

    // make() mit Capacity-Hint (kein Limit, nur Optimierung)
    d := make(map[string]int, 1000)
    _ = d

    fmt.Println(b, c)
}
Output
a == nil? true len: 0
a["x"]: 0
map[x:1] map[x:1 y:2]

Die make-Variante mit Größen-Hint (make(map[K]V, n)) ist eine reine Optimierungs-Hilfe: Go reserviert intern Buckets für etwa n Einträge und vermeidet damit frühe Re-Hashes. Eine Größenbegrenzung ist das nicht — du kannst beliebig mehr Einträge schreiben.

Lesen, Schreiben, Löschen

Maps werden über Index-Syntax angesprochen — wie Slices, nur dass der Index hier ein Key beliebigen comparable-Typs ist.

Go map_crud.go
package main

import "fmt"

func main() {
    scores := map[string]int{"Alice": 42}

    // Schreiben (insert oder update)
    scores["Bob"] = 17
    scores["Alice"] = 99 // überschreibt

    // Lesen — Zero-Value bei fehlendem Key
    fmt.Println(scores["Alice"])  // 99
    fmt.Println(scores["Carol"])  // 0  — niemals KeyError, niemals panic

    // Löschen
    delete(scores, "Bob")
    delete(scores, "ExistsNicht") // No-Op, kein Fehler

    // Komplett leeren (Go 1.21+)
    // clear(scores)

    fmt.Println(scores, "len:", len(scores))
}
Output
99
0
map[Alice:99] len: 1

delete(m, key) ist eingebaut, gibt nichts zurück und ist ein No-Op, wenn der Key nicht existiert. Mit Go 1.21 kam zusätzlich das eingebaute clear(m), das alle Einträge entfernt, ohne die Map neu zu allokieren.

Das comma-ok Idiom

Weil ein Lookup auf einen fehlenden Key den Zero-Value liefert, kannst du nicht ohne Weiteres unterscheiden, ob der Key fehlt oder ob der gespeicherte Wert tatsächlich 0 (oder "", oder nil) ist. Genau dafür gibt es die Zwei-Wert-Form:

Go map_commaok.go
package main

import "fmt"

func main() {
    visits := map[string]int{
        "/home":   10,
        "/about":  0, // tatsächlich gespeicherter Nullwert
    }

    // Einfacher Lookup — kann nicht zwischen "fehlt" und "ist 0" unterscheiden
    fmt.Println(visits["/home"], visits["/about"], visits["/missing"])

    // comma-ok: v ist Wert, ok ist bool
    if v, ok := visits["/about"]; ok {
        fmt.Println("/about existiert mit Wert", v)
    }

    if _, ok := visits["/missing"]; !ok {
        fmt.Println("/missing fehlt")
    }
}
Output
10 0 0
/about existiert mit Wert 0
/missing fehlt

Das comma-ok-Idiom ist die idiomatische Existenz-Prüfung in Go — und identisch zur Form bei Type-Assertions und Channel-Receives.

Iteration mit range

Über eine Map iterierst du mit for ... range. Wichtig zu verinnerlichen: Die Reihenfolge ist nicht definiert und absichtlich randomisiert — verlasse dich nie auf sie.

Go map_range.go
package main

import (
    "fmt"
    "sort"
)

func main() {
    ages := map[string]int{
        "Alice": 30, "Bob": 25, "Carol": 35,
    }

    // Beide Werte
    for name, age := range ages {
        fmt.Printf("%s ist %d\n", name, age)
    }

    // Nur Keys
    for name := range ages {
        _ = name
    }

    // Deterministische Reihenfolge: Keys einsammeln und sortieren
    keys := make([]string, 0, len(ages))
    for k := range ages {
        keys = append(keys, k)
    }
    sort.Strings(keys)
    for _, k := range keys {
        fmt.Println(k, "->", ages[k])
    }
}

Die Randomisierung wurde in Go 1.0 eingebaut, weil Programme sich sonst stillschweigend auf eine zufällige Reihenfolge der jeweiligen Hash-Implementierung verlassen — und beim nächsten Compiler-Update brechen.

Map-Größe — len() statt cap()

len(m) gibt die Anzahl der gespeicherten Einträge zurück. Anders als bei Slices funktioniert cap() bei Maps nicht — der Aufruf ist sogar ein Compile-Error.

Go map_len.go
package main

import "fmt"

func main() {
    m := make(map[string]int, 100) // Hint = 100, kein Limit
    m["a"] = 1
    m["b"] = 2

    fmt.Println("len:", len(m)) // 2

    // fmt.Println(cap(m))  // <-- Compile-Error:
    //   invalid argument: cap requires array, slice, channel, or array pointer

    var n map[string]int
    fmt.Println("len(nil):", len(n)) // 0 — len auf nil-Map ist OK
}
Output
len: 2
len(nil): 0

Auch praktisch: len() auf einer nil-Map ist legal und liefert 0. Du brauchst keine separate nil-Prüfung vor dem Längen-Check.

Nested Maps und Map-of-Slices

Maps können beliebige Wert-Typen enthalten — auch andere Maps oder Slices. Das ist nützlich, hat aber eine Falle: innere Maps und Slices musst du selbst initialisieren, bevor du sie befüllst.

Go map_nested.go
package main

import "fmt"

func main() {
    // Map-of-Map: User-ID -> Settings
    settings := map[int]map[string]string{}

    // settings[1]["theme"] = "dark"  // <-- panic: nil-Map zuweisen

    // Innere Map muss zuerst angelegt werden
    settings[1] = map[string]string{}
    settings[1]["theme"] = "dark"

    // Map-of-Slice: Tag -> URLs
    byDay := map[string][]string{}
    byDay["mo"] = append(byDay["mo"], "/home") // append auf nil-Slice ist OK
    byDay["mo"] = append(byDay["mo"], "/about")

    fmt.Println(settings)
    fmt.Println(byDay)
}
Output
map[1:map[theme:dark]]
map[mo:[/home /about]]

Bei Map-of-Slice rettet dich append: ein nil-Slice ist eine völlig legale Basis für append. Bei Map-of-Map gibt es diesen Komfort nicht — nil-Map plus Schreibzugriff ist sofort panic.

Erlaubte Key-Typen — comparable

Map-Keys müssen mit == und != vergleichbar sein. Die Sprach-Spec listet das so:

The comparison operators == and != must be fully defined for operands of the key type; thus the key type must not be a function, map, or slice.

Konkret erlaubt sind:

  • Boolean, numerische Typen, Strings — alle vergleichbar.
  • Pointer und Channels — verglichen wird die Adresse.
  • Interface-Typen — verglichen wird (dynamic type, dynamic value). Wenn der dynamische Typ nicht comparable ist, gibt es einen Runtime-panic.
  • Structs und Arrays — comparable, sofern alle Felder/Elemente comparable sind.

Nicht erlaubt als Key sind: slice, map, func. Bei diesen Typen gibt es schon zur Compile-Zeit einen Fehler.

Go map_keys.go
package main

import "fmt"

type Coord struct {
    X, Y int
}

func main() {
    // Struct als Key — clean und idiomatisch
    grid := map[Coord]string{
        {0, 0}: "Ursprung",
        {1, 2}: "Ziel",
    }
    fmt.Println(grid[Coord{0, 0}])

    // Set-Pattern: map[T]struct{} spart Speicher gegenüber map[T]bool
    seen := map[string]struct{}{}
    seen["a"] = struct{}{}
    _, ok := seen["a"]
    fmt.Println("gesehen:", ok)

    // Folgendes wäre ein Compile-Error:
    // var bad map[[]int]string
    //   invalid map key type []int
}
Output
Ursprung
gesehen: true

Der Trick mit map[T]struct{} ist die idiomatische Set-Implementierung: struct{} belegt 0 Bytes, der Set hat also nur den Overhead der Hash-Buckets selbst.

Das maps-Paket (Go 1.21+)

Mit Go 1.21 kam das Paket maps in die Standardbibliothek — generische Helfer, die vorher jeder selbst geschrieben hat.

FunktionZweck
maps.Clone(m)Flacher Klon (gleiche Wert-Pointer, neue Map)
maps.Copy(dst, src)Alle Paare aus src in dst schreiben
maps.Equal(a, b)True, wenn beide Maps gleiche Paare enthalten
maps.EqualFunc(a, b, eq)Wie Equal, aber mit eigener Vergleichsfunktion
maps.DeleteFunc(m, pred)Alle Paare löschen, für die pred true liefert
maps.Keys(m) (1.23+)Iterator (iter.Seq) über alle Keys
maps.Values(m) (1.23+)Iterator über alle Werte
maps.All(m) (1.23+)Iterator (iter.Seq2) über Key-Value-Paare
Go map_pkg.go
package main

import (
    "fmt"
    "maps"
)

func main() {
    a := map[string]int{"x": 1, "y": 2}
    b := maps.Clone(a)

    fmt.Println("equal?", maps.Equal(a, b))

    // Alle geraden Werte entfernen
    maps.DeleteFunc(b, func(_ string, v int) bool {
        return v%2 == 0
    })
    fmt.Println("nach DeleteFunc:", b)

    // Mergen: alles aus a in b kippen
    maps.Copy(b, a)
    fmt.Println("nach Copy:", b)
}
Output
equal? true
nach DeleteFunc: map[x:1]
nach Copy: map[x:1 y:2]

Beachte: maps.Clone ist flach — Pointer-Werte und innere Referenz-Typen werden geteilt. Wer eine Tiefen-Kopie braucht, muss selbst rekursiv klonen.

Häufige Stolperfallen

Schreiben in eine nil-Map ist ein Panic.

var m map[string]int ist eine deklarierte, aber uninitialisierte Map — ihr Wert ist nil. Lesen funktioniert (du bekommst den Zero-Value zurück), len() liefert 0 — aber jeder Schreibzugriff m[k] = v beendet das Programm mit „assignment to entry in nil map”. Initialisiere Maps immer mit make() oder einem Literal, bevor du schreibst.

Iterations-Reihenfolge ist randomisiert — und zwar absichtlich.

Zwei aufeinanderfolgende range-Schleifen über dieselbe Map können verschiedene Reihenfolgen liefern. Die Go-Runtime startet jedes range bei einem zufälligen Bucket, damit kein Programm versehentlich eine Reihenfolgen-Annahme einbaut. Brauchst du eine deterministische Reihenfolge, sortiere die Keys vorher in ein Slice.

Pointer auf Map-Werte sind illegal.

&m[“key”] ist ein Compile-Error: „cannot take the address of m[“key”]”. Grund: Maps können beim Re-Hash ihre Buckets verschieben — ein gespeicherter Pointer würde plötzlich ins Nirgendwo zeigen. Wenn du Mutation brauchst, speichere stattdessen map[K]*V mit explizit allokierten Pointern.

Während range schreiben ist erlaubt — aber unzuverlässig.

Du darfst während einer range-Iteration in dieselbe Map schreiben oder löschen, ohne dass Go panict. Aber die Spec macht keine Garantie, ob neu eingefügte Keys von der laufenden Iteration noch gesehen werden — manchmal ja, manchmal nein. Sammle Änderungen lieber in einer zweiten Map und merge nach dem range.

Concurrent Read/Write paniciert hart.

Maps sind nicht für nebenläufigen Zugriff ausgelegt. Seit Go 1.6 erkennt die Runtime gleichzeitige Lese- und Schreibzugriffe aktiv und beendet das Programm mit „concurrent map writes” — auch ohne Race-Detector. Lösung: ein sync.RWMutex davor, oder sync.Map für Sonderfälle wie write-once-then-read-many.

Nicht-comparable Keys produzieren Compile-Errors.

map[[]int]string oder map[func()]int compiliert nicht — Slices, Maps und Funktionen sind nicht mit == vergleichbar. Bei Interfaces als Key-Typ wird das verschoben: Compile geht durch, aber zur Laufzeit gibt es einen panic, sobald ein nicht-comparable dynamischer Typ auftaucht.

Maps werden als Header übergeben — Mutationen sind sichtbar.

Ein Map-Wert ist intern ein kleiner Header (≈ Slice), der auf die eigentliche Hashtabelle zeigt. Übergibst du eine Map an eine Funktion, kopiert Go nur den Header — beide Seiten arbeiten auf denselben Daten. Wenn die Funktion m[k] = v macht, sieht der Caller die Änderung. Ein *map[K]V-Parameter ist deshalb fast nie nötig und gilt als unidiomatisch.

Nested-Map-Mutation ist tricky.

Bei m := map[string]map[string]int kannst du m[“a”][“b”] = 1 nicht direkt schreiben — die innere Map ist nil. Du brauchst entweder eine Helper-Funktion, die fehlende innere Maps anlegt, oder du wechselst zu einem zusammengesetzten Key wie map[Key]int mit type Key struct{ A, B string }. Der Struct-Key ist meistens die saubere Variante.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Datentypen

Zur Übersicht