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
rangeist 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:
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))
}Berlin
Anzahl Länder: 3Map erstellen — make, Literal oder nil
Es gibt drei Wege, eine Map ins Leben zu rufen — mit unterschiedlichen Konsequenzen:
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)
}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.
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))
}99
0
map[Alice:99] len: 1delete(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:
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")
}
}10 0 0
/about existiert mit Wert 0
/missing fehltDas 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.
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.
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
}len: 2
len(nil): 0Auch 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.
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)
}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.
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
}Ursprung
gesehen: trueDer 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.
| Funktion | Zweck |
|---|---|
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 |
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)
}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
- Map types – Go Language Specification
- Making maps – Go Specification
- Paket maps – pkg.go.dev (Go 1.21+)
- Paket sync – sync.Map
- Go maps in action – Go-Blog