Das Keyword map deklariert die eingebaute Hashtabelle von Go. Der Typ map[K]V ordnet Keys vom Typ K Werte vom Typ V zu, mit im Mittel konstanter Zeit für Lesen, Einfügen und Löschen. Maps sind Teil der Sprache. Du importierst also kein Package und instanziierst keine HashMap-Klasse. Stattdessen gibt es eine Literal-Syntax, die Builtins make, delete, clear und len sowie Unterstützung für for range (Go spec).
Kurzfassung
map[string]int{"/healthz": 1}odermake(map[string]int)erzeugt eine Map.var m map[string]intliefert dir einenil-Map.- Das Lesen aus einer
nil-Map gibt den Nullwert zurück. Das Schreiben in einenil-Map löst eine Panic mitassignment to entry in nil mapaus. - Ein fehlender Key liefert den Nullwert. Verwende
v, ok := m[k], wenn du wissen musst, ob der Key vorhanden war. - Die Iterationsreihenfolge ist nicht festgelegt und wird zufällig gewählt. Sortiere die Keys mit
slices.Sorted(maps.Keys(m)), wenn die Reihenfolge wichtig ist. - Keys müssen vergleichbar sein. Strings, Zahlen, Structs und Arrays funktionieren. Slices, Maps und Funktionen nicht.
- Ein Map-Wert ist ein kleiner Deskriptor. Übergibst du ihn an eine Funktion, kann sie die Map des Aufrufers verändern.
m[k].Field = xkompiliert nicht, wenn der Wert ein Struct ist. Kopiere den Wert, ändere ihn und speichere ihn zurück, oder speichere Pointer.- Maps sind nicht sicher für nebenläufige Schreibzugriffe. Schütze sie mit einem
sync.Mutexoder verwendesync.Mapfür die Fälle, für die es gebaut wurde. map[string]struct{}ist das Set von Go. Das Packagemaps(ab Go 1.21) bringtClone,Equal,DeleteFunc,KeysundCollectmit.
Wie deklariert und erzeugt man eine Map in Go?
Ein Map-Typ wird als map[KeyType]ValueType geschrieben. Eine Map erzeugst du mit einem Composite Literal, wenn du einige Einträge schon vorher kennst, oder mit make, wenn du leer anfängst:
example.gogopackage main import "fmt" func main() { statusText := map[int]string{ 200: "OK", 404: "Not Found", 503: "Service Unavailable", } hits := make(map[string]int) hits["/api/orders"]++ hits["/api/orders"]++ hits["/healthz"]++ fmt.Println(statusText[404], len(statusText)) fmt.Println(hits["/api/orders"], hits["/healthz"], len(hits)) }
example.texttextNot Found 3 2 1 2
hits["/api/orders"]++ funktioniert auch mit einem Key, den es noch nicht gibt. Das Lesen liefert 0, das Inkrement macht daraus 1, und das Schreiben speichert den Wert. Ein Zähler braucht keine Vorbereitung.
make nimmt optional einen Größenhinweis entgegen, etwa make(map[string]int, len(requests)). Die Runtime reserviert Platz für ungefähr so viele Einträge, damit die Map beim Befüllen nicht wachsen muss. Der Hinweis ist keine Obergrenze. Die Map wächst trotzdem darüber hinaus. len(m) gibt die Anzahl der Einträge zurück, und cap funktioniert bei Maps nicht.
var vs. make in Go vergleicht die beiden Wege zu einer leeren Map und erklärt, warum var allein dafür nicht reicht.
Warum löst das Schreiben in eine nil-Map eine Panic aus?
Der Nullwert eines Map-Typs ist nil. Hinter einer nil-Map steht keine Hashtabelle, also gibt es keinen Ort für einen neuen Eintrag. Meist begegnet dir das als Map-Feld in einem Struct, das niemand initialisiert hat:
example.gogopackage main import "fmt" type Router struct { routes map[string]string } func main() { var r Router fmt.Println(r.routes["/health"] == "", len(r.routes)) r.routes["/health"] = "healthHandler" }
example.texttexttrue 0 panic: assignment to entry in nil map
Lesen funktioniert. Bei Lookups, len, range und delete verhält sich eine nil-Map wie eine leere Map, deshalb wird die erste Zeile normal ausgegeben. Nur das Schreiben löst die Panic aus. Erzeuge die Map deshalb vor dem ersten Schreibzugriff, meist in einem Konstruktor:
example.gogofunc NewRouter() *Router { return &Router{routes: make(map[string]string)} }
Ein Decoder wie json.Unmarshal legt die Map selbst an, wenn er auf ein JSON-Objekt trifft. Ein Ziel vom Typ map[string]any darf also als nil beginnen. Dein eigener Code muss make aufrufen oder ein Literal verwenden.
Wie prüft man, ob ein Key in einer Go-Map existiert?
Greifst du mit einem fehlenden Key auf eine Map zu, bekommst du den Nullwert des Werttyps. Bei einem Zähler ist das genau richtig, denn ein Pfad, den niemand aufgerufen hat, hat 0 Hits. Bei anderen Werten verdeckt der Nullwert einen echten Unterschied. Eine Map mit Feature-Flags liefert false für ein ausgeschaltetes Flag und ebenso für ein Flag, das nie definiert wurde.
Die comma-ok-Form gibt einen zweiten booleschen Wert zurück, der dir sagt, ob der Key vorhanden war:
example.gogopackage main import "fmt" func main() { flags := map[string]bool{ "new_checkout": true, "dark_mode": false, } for _, name := range []string{"new_checkout", "dark_mode", "beta_search"} { enabled, ok := flags[name] if !ok { fmt.Printf("%s: unknown flag\n", name) continue } fmt.Printf("%s: %v\n", name, enabled) } }
example.texttextnew_checkout: true dark_mode: false beta_search: unknown flag
ok ist nur für beta_search false. Ohne diese Prüfung würde ein Tippfehler im Flag-Namen das Feature stillschweigend abschalten. Nimm die einfache Form, wenn der Nullwert eine korrekte Antwort ist, etwa bei Zählern und Summen. Nimm comma-ok, wenn „fehlt“ etwas anderes bedeutet als „null“.
Wie löscht man Keys und leert eine Map?
delete(m, k) entfernt einen Key. Einen Key zu löschen, den es nicht gibt, bewirkt nichts und ist kein Fehler. clear(m), eingeführt mit Go 1.21, entfernt alle Einträge und behält die Map im Speicher (Go 1.21 Release Notes):
example.gogocache := map[int]string{42: "[email protected]", 7: "[email protected]"} delete(cache, 42) delete(cache, 999) fmt.Println(len(cache), cache) clear(cache) fmt.Println(len(cache), cache == nil)
example.texttext1 map[7:[email protected]] 0 false
Nach clear ist die Map leer, aber weiter verwendbar, und jede Variable, die auf sie verweist, sieht die leere Map. Weist du stattdessen cache = map[int]string{} zu, bekommt nur diese eine Variable eine neue Map.
Keys zu löschen, während du über dieselbe Map iterierst, ist erlaubt. Laut Spec wird ein Eintrag, der während der Iteration entfernt wird, später in der Schleife nicht mehr geliefert (Go spec). Auch das Hinzufügen von Keys während der Iteration ist erlaubt. Ein neuer Eintrag kann in derselben Schleife aber auftauchen oder auch nicht.
Warum ist die Iterationsreihenfolge von Maps in Go zufällig?
Laut Spec ist die Reihenfolge beim Iterieren über eine Map nicht festgelegt, und sie ist auch nicht garantiert von einem Durchlauf zum nächsten gleich. Die Runtime geht noch weiter und wählt bei jedem range einen zufälligen Startpunkt. Drei Durchläufe derselben Schleife über hits lieferten:
example.texttext/api/orders 41 /healthz 120 /api/users 17 /api/orders 41 /healthz 120 /api/users 17 /api/users 17 /api/orders 41 /healthz 120
Die Zufälligkeit ist Absicht. In frühen Go-Versionen wirkte die Reihenfolge bei kleinen Maps stabil, und Programme verließen sich darauf. Als sich die Implementierung änderte, gingen diese Programme kaputt. Durch die zufällige Reihenfolge zeigt sich der Bug in deinen Tests statt in Produktion (Go-Blog, Go maps in action).
Brauchst du eine stabile Reihenfolge, etwa für eine Logzeile, einen Report oder einen Test, dann sortiere die Keys. Seit Go 1.23 gibt maps.Keys einen Iterator zurück, und slices.Sorted sammelt und sortiert ihn in einem Aufruf:
example.gogopackage main import ( "fmt" "maps" "slices" ) func main() { hits := map[string]int{"/api/orders": 41, "/healthz": 120, "/api/users": 17} for _, path := range slices.Sorted(maps.Keys(hits)) { fmt.Printf("%-12s %d\n", path, hits[path]) } }
example.texttext/api/orders 41 /api/users 17 /healthz 120
fmt.Println gibt die Keys einer ganzen Map bereits sortiert aus. Deshalb war die Ausgabe von fmt.Println(cache) weiter oben stabil. Auch encoding/json sortiert Map-Keys beim Marshalling. Zufällig ist nur range.
Welche Typen können Keys einer Go-Map sein?
Ein Key-Typ muss vergleichbar sein, das heißt, == und != müssen für ihn definiert sein. Strings, Ganzzahlen, Gleitkommazahlen, Booleans, Pointer, Channels und Interfaces sind vergleichbar. Das gilt auch für Structs und Arrays, deren Felder oder Elemente alle vergleichbar sind. Slices, Maps und Funktionen sind es nicht, und der Compiler lehnt sie ab:
example.gogoseen := map[[]string]bool{}
example.texttext./main.go:4:14: invalid map key type []string
Struct-Keys sind praktisch, wenn der Key aus mehreren Teilen besteht. Hier ist die Latenz jedes Endpunkts nach Methode und Pfad zusammen abgelegt:
example.gogotype Endpoint struct { Method string Path string } latencyMs := map[Endpoint]int{ {"GET", "/api/orders"}: 38, {"POST", "/api/orders"}: 112, } fmt.Println(latencyMs[Endpoint{"POST", "/api/orders"}])
Das gibt 112 aus. Das Struct wird Feld für Feld verglichen, also finden zwei Endpoint-Werte mit derselben Methode und demselben Pfad denselben Eintrag. Damit entfällt der alte Trick, einen Key-String wie "POST /api/orders" zu bauen und ihn später wieder aufzuteilen. Willst du nach einer Liste von IDs indizieren, wandle den Slice in ein Array fester Größe um oder füge ihn zu einem String zusammen.
Interface-Keys wie map[any]int kompilieren, aber das Einfügen eines Slice löst zur Laufzeit eine Panic mit runtime error: hash of unhashable type []string aus. Float-Keys funktionieren, sind aber eine schlechte Idee. NaN ist nicht gleich sich selbst, also legt jedes m[math.NaN()] = 1 einen neuen Eintrag an, den du nie wieder abfragen kannst.
Werden Maps in Go per Referenz übergeben?
Go übergibt alles per Wert, auch Maps. Der Wert einer Map-Variable ist ein kleiner Deskriptor, der auf die Hashtabelle zeigt. Beim Kopieren wird also der Pointer kopiert, nicht die Einträge. Eine Funktion, die eine Map erhält, kann die Einträge des Aufrufers ändern. Eine Funktion, die ihrem Parameter eine neue Map zuweist, ändert nur ihre eigene Kopie:
example.gogofunc recordStatus(counts map[int]int, status int) { counts[status]++ } func resetCounts(counts map[int]int) { counts = map[int]int{} } func main() { counts := map[int]int{} recordStatus(counts, 200) recordStatus(counts, 500) resetCounts(counts) fmt.Println(counts) }
example.texttextmap[200:1 500:1]
recordStatus hat über die gemeinsame Tabelle geschrieben, deshalb sind beide Zähler vorhanden. resetCounts hat seine lokale Variable ersetzt und beim Aufrufer nichts bewirkt. Um die Map des Aufrufers zu leeren, rufst du in der Funktion clear(counts) auf. go vet meldet das nicht, also kompiliert die zweite Funktion und tut stillschweigend nichts.
Das gilt genauso für Struct-Felder und Rückgabewerte. Ein Getter, der eine interne Map zurückgibt, erlaubt Aufrufern, deinen Zustand zu verändern. Gib maps.Clone(m) zurück, wenn Aufrufer eine eigene Kopie bekommen sollen.
Warum kann ich einem Struct-Feld in einer Map nichts zuweisen?
Ein Map-Wert ist nicht adressierbar. Die Runtime kann Einträge verschieben, wenn die Tabelle wächst, deshalb gibt Go dir keine Referenz auf einen darin gespeicherten Wert. Dieser Code kompiliert daher nicht:
example.gogotype Session struct { UserID int Requests int } sessions := map[string]Session{"s_91": {UserID: 42}} sessions["s_91"].Requests++
example.texttext./main.go:10:2: cannot assign to struct field sessions["s_91"].Requests in map
Es gibt zwei Lösungen. Kopiere den Wert heraus, ändere ihn und speichere ihn zurück. Oder speichere Pointer, sodass die Map Adressen hält und das Struct außerhalb der Tabelle liegt:
example.gogos := sessions["s_91"] s.Requests++ sessions["s_91"] = s byToken := map[string]*Session{"s_91": {UserID: 42}} byToken["s_91"].Requests++
In beiden Fällen steht Requests danach auf 1. Beim Kopieren und Zurückspeichern bleibt die Map alleinige Besitzerin der Daten. Mit Pointern ist das Update kürzer, und große Structs werden nicht kopiert. Ein fehlender Key liefert dir aber einen nil-Pointer, und byToken["s_404"].Requests++ löst eine Panic aus. Prüfe deshalb vorher mit comma-ok. Aus demselben Grund kannst du &sessions["s_91"] nicht bilden.
Sind Go-Maps sicher für nebenläufige Zugriffe?
Nein. Nebenläufige Lesezugriffe sind in Ordnung, aber ein Schreibzugriff parallel zu einem anderen Lese- oder Schreibzugriff ist ein Data Race. Die Runtime erkennt einige dieser Races und hält das ganze Programm mit einem fatalen Fehler an, den recover nicht abfangen kann. Acht Goroutinen, die einen gemeinsamen Zähler in einer Map erhöhen, stürzen sofort ab:
example.gogohits := map[string]int{} var wg sync.WaitGroup for range 8 { wg.Go(func() { for range 10000 { hits["/api/orders"]++ } }) } wg.Wait()
example.texttextfatal error: concurrent map writes
Ein Lesezugriff, der mit einem Schreibzugriff kollidiert, ergibt fatal error: concurrent map read and map write. In einem HTTP-Server passiert das, sobald zwei Requests eine gemeinsame Map anfassen, denn jeder Request läuft in seiner eigenen Goroutine. Führe deine Tests mit go test -race aus, um Races zu finden, die der Runtime-Check übersieht.
Die übliche Lösung ist, die Map und einen sync.Mutex in einen Typ zu packen und die Map nur über Methoden anzufassen, die sperren:
example.gogotype HitCounter struct { mu sync.Mutex hits map[string]int } func NewHitCounter() *HitCounter { return &HitCounter{hits: make(map[string]int)} } func (c *HitCounter) Inc(path string) { c.mu.Lock() defer c.mu.Unlock() c.hits[path]++ } func (c *HitCounter) Get(path string) int { c.mu.Lock() defer c.mu.Unlock() return c.hits[path] }
Dieselben acht Goroutinen laufen jetzt durch, und c.Get("/api/orders") gibt 80000 zurück. Verwende sync.RWMutex mit RLock in Get, wenn Lesezugriffe weit überwiegen und jeder Lesezugriff echte Arbeit macht.
sync.Map ist eine nebenläufige Map aus der Standardbibliothek. Laut Dokumentation ist sie für zwei Fälle optimiert: Keys, die einmal geschrieben und oft gelesen werden, wie bei einem Cache, der nur wächst, und Goroutinen, die mit disjunkten Mengen von Keys arbeiten (sync.Map-Dokumentation). Außerhalb dieser Fälle ist eine normale Map mit Mutex meist einfacher und genauso schnell. Dazu behältst du deine Key- und Werttypen, statt mit any zu arbeiten.
Wie baut man ein Set in Go?
Go hat keinen Set-Typ. Das Idiom ist eine Map mit einem leeren Struct als Wert. struct{} belegt null Bytes, also speichert die Map nur die Keys. Dieser Code entfernt doppelte eingehende Requests anhand ihrer ID:
example.gogorequestIDs := []string{"req_7f", "req_a1", "req_7f", "req_c3", "req_a1"} seen := make(map[string]struct{}, len(requestIDs)) for _, id := range requestIDs { if _, dup := seen[id]; dup { fmt.Println("duplicate, skipping", id) continue } seen[id] = struct{}{} } fmt.Println(len(seen), "unique requests")
example.texttextduplicate, skipping req_7f duplicate, skipping req_a1 3 unique requests
map[string]bool funktioniert auch und liest sich etwas leichter, weil if seen[id] ohne comma-ok auskommt. Es kostet aber ein Byte pro Eintrag und erlaubt einen dritten Zustand, nämlich einen Key, der mit false gespeichert ist. Entscheide dich pro Codebasis für eine Variante. Das Go-Keyword struct behandelt struct{} und seinen zweiten Einsatz als Signal auf Channels.
Was macht das Package maps?
Das Package maps kam mit Go 1.21 und bringt generische Hilfsfunktionen mit. Go 1.23 hat Iterator-Funktionen ergänzt (pkg.go.dev/maps). Diese kommen am häufigsten vor:
example.gogodefaults := map[string]string{"region": "eu-west-1", "log_level": "info"} cfg := maps.Clone(defaults) cfg["log_level"] = "debug" cfg["debug_token"] = "tok_123" fmt.Println(maps.Equal(cfg, defaults)) maps.DeleteFunc(cfg, func(k, v string) bool { return strings.HasPrefix(k, "debug_") }) fmt.Println(slices.Sorted(maps.Keys(cfg))) fmt.Println(defaults["log_level"]) roles := maps.Collect(slices.All([]string{"admin", "editor", "viewer"})) fmt.Println(roles[1])
example.texttextfalse [log_level region] info editor
maps.Cloneerstellt eine flache Kopie. Die Änderungen ancfghabendefaultsnicht berührt. Die Werte selbst werden nicht tief kopiert, eine Map von Slices teilt sich also weiterhin die Slices.maps.Equalvergleicht Keys und Werte mit==. Zwei Maps kannst du nicht direkt mit==vergleichen, denn Maps lassen sich nur mitnilvergleichen.maps.DeleteFuncentfernt jeden Eintrag, auf den ein Prädikat zutrifft.maps.Keys,maps.Valuesundmaps.Allgeben Iteratoren zurück. Übergib sie anslices.Sorted,slices.Collectoder einefor range-Schleife.maps.Collectbaut eine Map aus einem beliebigen Key-Value-Iterator. Hier macht es aus einem Slice eine Map von Index zu Wert.maps.Copy(dst, src)führt eine Map in eine andere zusammen.
Wie sind Go-Maps implementiert?
Seit Go 1.24 verwendet die eingebaute Map ein Swiss-Table-Design, eine Hashtabelle mit offener Adressierung, die Gruppen von Slots auf einmal prüft. Die Release Notes melden weniger CPU-Aufwand für Map-Operationen (Go 1.24 Release Notes). Die Sprache hat sich nicht geändert, deshalb läuft Code, der für die älteren Bucket-basierten Maps geschrieben wurde, wie bisher.
Maps schrumpfen weiterhin nie. Gelöschte Einträge geben ihre Slots zur Wiederverwendung frei, der Speicher bleibt aber belegt. Ein Cache, der einmal eine Million Sessions gehalten hat, behält diesen Speicher, auch nachdem du sie gelöscht hast. Auch clear gibt ihn nicht frei. Wächst eine langlebige Map stark an und schrumpft dann wieder, kopiere die verbliebenen Einträge mit maps.Clone oder einem neuen make in eine neue Map und lass die alte vom Garbage Collector einsammeln.
Wo LevelUpGo ins Spiel kommt
LevelUpGo bringt dir Go mit Übungen bei, die echten Go-Code im Browser ausführen. Composite Types behandelt Maps neben Slices und Structs, einschließlich comma-ok-Lookups, Zählen, Löschen und Iterieren in stabiler Reihenfolge. Concurrency Fundamentals behandelt Goroutinen, WaitGroups und die Mutexe, die du brauchst, sobald sich mehrere Goroutinen eine Map teilen. Die anderen 24 reservierten Wörter findest du in Go Keywords: Alle 25 erklärt.
Häufig gestellte Fragen
Ist map ein Keyword in Go?
Ja. map ist eines der 25 reservierten Keywords von Go, du kannst also keine Variable und keine Funktion map nennen. Es kommt in Map-Typen wie map[string]int vor, in Map-Literalen und als Argument von make.
Ist eine Go-Map geordnet?
Nein. Die Spec lässt die Iterationsreihenfolge offen, und die Runtime wählt sie bei jedem range zufällig. Um Keys in einer bestimmten Reihenfolge zu verarbeiten, sortiere sie mit slices.Sorted(maps.Keys(m)). fmt.Println und json.Marshal geben Map-Keys bereits sortiert aus.
Sind Go-Maps threadsicher?
Nein. Nebenläufige Lesezugriffe sind sicher, aber jeder Schreibzugriff neben einem anderen Lese- oder Schreibzugriff kann das Programm mit fatal error: concurrent map writes abstürzen lassen. Schütze die Map mit einem sync.Mutex oder sync.RWMutex, oder verwende sync.Map für Caches, die einmal geschrieben werden, und für disjunkte Key-Mengen.
Wie bekommt man die Länge einer Map in Go?
Mit len(m). Es gibt die Anzahl der Einträge zurück und funktioniert auch bei einer nil-Map, wo es 0 liefert. Maps unterstützen cap nicht.
Hat Go einen Set-Typ?
Nein. Verwende map[T]struct{}, das nur Keys speichert, und prüfe die Mitgliedschaft mit _, ok := set[k]. map[T]bool funktioniert auch, wenn dir if set[k] lieber ist als comma-ok.
Was passiert, wenn man einen Key liest, der nicht existiert?
Du bekommst den Nullwert des Werttyps, etwa 0, "", false oder nil, und keinen Fehler. Verwende die Form mit zwei Werten, v, ok := m[k], wenn du einen fehlenden Key von einem gespeicherten Nullwert unterscheiden musst.
Quellen
- The Go Programming Language Specification, Map types: https://go.dev/ref/spec#Map_types
- The Go Programming Language Specification, Index expressions: https://go.dev/ref/spec#Index_expressions
- The Go Programming Language Specification, Deletion of map elements: https://go.dev/ref/spec#Deletion_of_map_elements
- The Go Programming Language Specification, For statements with range clause: https://go.dev/ref/spec#For_range
- The Go Blog, Go maps in action: https://go.dev/blog/maps
- Go 1.21 release notes: https://go.dev/doc/go1.21
- Go 1.23 release notes: https://go.dev/doc/go1.23
- Go 1.24 release notes: https://go.dev/doc/go1.24
- maps package: https://pkg.go.dev/maps
- sync.Map: https://pkg.go.dev/sync#Map
