The map keyword declares Go's built-in hash table. The type map[K]V maps keys of type K to values of type V, with average constant-time lookup, insert and delete. Maps are part of the language, so you don't import a package or instantiate a HashMap class. You get a literal syntax, the make, delete, clear and len builtins, and for range support (Go spec).
TL;DR
map[string]int{"/healthz": 1}ormake(map[string]int)creates a map.var m map[string]intgives you anilmap.- Reading a
nilmap returns the zero value. Writing to one panics withassignment to entry in nil map. - A missing key returns the zero value. Use
v, ok := m[k]when you need to know whether the key was there. - Iteration order is unspecified and randomized. Sort the keys with
slices.Sorted(maps.Keys(m))when order matters. - Keys must be comparable. Strings, numbers, structs and arrays work. Slices, maps and functions don't.
- A map value is a small descriptor. Passing it to a function lets the function modify the caller's map.
m[k].Field = xdoesn't compile when the value is a struct. Copy, modify and store it back, or store pointers.- Maps are not safe for concurrent writes. Guard them with a
sync.Mutexor usesync.Mapfor the cases it was built for. map[string]struct{}is Go's set. Themapspackage (Go 1.21+) addsClone,Equal,DeleteFunc,KeysandCollect.
How do you declare and create a map in Go?
A map type is written map[KeyType]ValueType. You create a map with a composite literal when you know some entries up front, or with make when you start empty:
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"]++ works on a key that doesn't exist yet. The read returns 0, the increment makes it 1, and the write stores it. A counter needs no setup.
make takes an optional size hint, as in make(map[string]int, len(requests)). The runtime allocates room for about that many entries, so the map doesn't have to grow while you fill it. The hint is not a limit. The map still grows past it. len(m) returns the number of entries, and cap doesn't work on maps.
var vs make in Go compares the two ways to get an empty map, and why var alone is not enough for one.
Why does writing to a nil map panic?
The zero value of a map type is nil. A nil map has no hash table behind it, so there is nowhere to put a new entry. This usually shows up as a map field in a struct that nobody initialized:
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
Reading works. A nil map behaves like an empty map for lookups, len, range and delete, so the first line prints normally. Only the write panics. The fix is to create the map before the first write, usually in a constructor:
example.gogofunc NewRouter() *Router { return &Router{routes: make(map[string]string)} }
A decoder such as json.Unmarshal allocates the map for you when it finds a JSON object, so a map[string]any target can start as nil. Your own code has to call make or use a literal.
How do you check if a key exists in a Go map?
Indexing a map with a missing key returns the zero value of the value type. For a counter that is exactly what you want, since a path nobody requested has 0 hits. For other values the zero value hides a real difference. A feature flag map returns false for a flag that is off and also for a flag that was never defined.
The comma-ok form returns a second boolean that tells you whether the key was present:
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 is false only for beta_search. Without it, a typo in a flag name would silently turn the feature off. Use the plain form when the zero value is a correct answer, like counts and sums. Use comma-ok when "missing" means something different from "zero".
How do you delete keys and clear a map?
delete(m, k) removes one key. Deleting a key that isn't there is a no-op, not an error. clear(m), added in Go 1.21, removes every entry and keeps the map allocated (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
After clear, the map is empty but still usable, and every variable that refers to it sees the empty map. Assigning cache = map[int]string{} instead gives only that one variable a new map.
Deleting keys while ranging over the same map is allowed. The spec says an entry removed during iteration will not be produced later in the loop (Go spec). Adding keys during iteration is also allowed, but a new entry may or may not show up in the same loop.
Why is map iteration order random in Go?
The spec says the iteration order over a map is not specified and is not guaranteed to be the same from one iteration to the next. The runtime goes further and picks a random starting point on every range. Three runs of the same loop over hits printed:
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
The randomization is deliberate. Early Go versions had an order that looked stable for small maps, and programs started depending on it. When the implementation changed, those programs broke. Randomizing the order makes the bug appear in your tests instead of in production (Go blog, Go maps in action).
When you need a stable order, for a log line, a report or a test, sort the keys. Since Go 1.23, maps.Keys returns an iterator and slices.Sorted collects and sorts it in one call:
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 of a whole map already prints keys in sorted order, which is why the output of fmt.Println(cache) above is stable. encoding/json also sorts map keys when it marshals. Only range is randomized.
What types can be Go map keys?
A key type must be comparable, which means == and != are defined for it. Strings, integers, floats, booleans, pointers, channels and interfaces are comparable. So are structs and arrays whose fields or elements are all comparable. Slices, maps and functions are not, and the compiler rejects them:
example.gogoseen := map[[]string]bool{}
example.texttext./main.go:4:14: invalid map key type []string
Struct keys are useful when the key has more than one part. Here the latency of each endpoint is keyed by method and path together:
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"}])
This prints 112. The struct is compared field by field, so two Endpoint values with the same method and path find the same entry. It avoids the older trick of building a key string like "POST /api/orders" and splitting it again later. To key by a list of IDs, convert the slice to a fixed-size array or join it into a string.
Interface keys such as map[any]int compile, but inserting a slice at runtime panics with runtime error: hash of unhashable type []string. Float keys work but are a bad idea. NaN is not equal to itself, so every m[math.NaN()] = 1 adds a new entry that you can never look up again.
Are maps passed by reference in Go?
Go passes everything by value, maps included. The value of a map variable is a small descriptor that points to the hash table, so copying it copies the pointer and not the entries. A function that receives a map can change the caller's entries. A function that assigns a new map to its parameter only changes its own copy:
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 wrote through the shared table, so both counts are there. resetCounts replaced its local variable and had no effect on the caller. To empty the caller's map, call clear(counts) inside the function. go vet doesn't flag this, so the second function compiles and silently does nothing.
The same sharing applies to struct fields and return values. A getter that returns an internal map lets callers modify your state. Return maps.Clone(m) when callers should get their own copy.
Why can't I assign to a struct field in a map?
A map value is not addressable. The runtime can move entries when the table grows, so Go won't hand you a reference to a value stored inside it. That makes this fail to compile:
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
There are two fixes. Copy the value out, change it and store it back. Or store pointers, so the map holds addresses and the struct lives outside the table:
example.gogos := sessions["s_91"] s.Requests++ sessions["s_91"] = s byToken := map[string]*Session{"s_91": {UserID: 42}} byToken["s_91"].Requests++
Both leave Requests at 1. The copy-and-store form keeps the map as the single owner of the data. The pointer form is shorter to update and avoids copying large structs, but a missing key gives you a nil pointer, so byToken["s_404"].Requests++ panics. Check with comma-ok first. The same rule is why you can't take &sessions["s_91"].
Are Go maps safe for concurrent use?
No. Concurrent reads are fine, but a write concurrent with any other read or write is a data race. The runtime detects some of these races and stops the whole program with a fatal error that recover can't catch. Eight goroutines incrementing a shared counter map crash right away:
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
A read racing with a write gives fatal error: concurrent map read and map write. In an HTTP server this happens as soon as two requests touch a shared map, since every request runs in its own goroutine. Run your tests with go test -race to catch races that the runtime check misses.
The usual fix is to put the map and a sync.Mutex in one type and only touch the map through methods that lock:
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] }
The same eight goroutines now finish and c.Get("/api/orders") returns 80000. Use sync.RWMutex with RLock in Get when reads far outnumber writes and each read does real work.
sync.Map is a concurrent map in the standard library. Its docs say it is optimized for two cases: keys that are written once and read many times, like a cache that only grows, and goroutines that work on disjoint sets of keys (sync.Map docs). Outside those cases, a plain map with a mutex is usually simpler and as fast, and it keeps your key and value types instead of any.
How do you make a set in Go?
Go has no set type. The idiom is a map with an empty struct as the value. struct{} takes zero bytes, so the map stores only keys. This deduplicates incoming requests by 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 also works and reads a little more easily, since if seen[id] needs no comma-ok. It costs one byte per entry and allows a third state, a key stored with false. Pick one per codebase. The Go struct keyword covers struct{} and its other use as a signal on channels.
What does the maps package do?
The maps package arrived in Go 1.21 with generic helpers, and Go 1.23 added iterator functions to it (pkg.go.dev/maps). These are the ones that come up most:
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.Clonemakes a shallow copy. Changingcfgleftdefaultsalone. The values themselves are not deep-copied, so a map of slices still shares the slices.maps.Equalcompares keys and values with==. You can't compare two maps with==directly, since maps can only be compared tonil.maps.DeleteFuncremoves every entry that matches a predicate.maps.Keys,maps.Valuesandmaps.Allreturn iterators. Pass them toslices.Sorted,slices.Collector afor rangeloop.maps.Collectbuilds a map from any key-value iterator. Here it turns a slice into an index-to-value map.maps.Copy(dst, src)merges one map into another.
How are Go maps implemented?
Since Go 1.24, the built-in map uses a Swiss Table design, an open-addressing hash table that checks groups of slots at once. The release notes report lower CPU overhead for map operations (Go 1.24 release notes). The language didn't change, so code written against the older bucket-based maps runs as before.
Maps still never shrink. Deleting entries frees the slots for reuse but keeps the memory. A cache that once held a million sessions keeps that memory after you delete them. clear doesn't release it either. If a long-lived map spikes and then drops, copy the survivors into a new map with maps.Clone or a fresh make and let the old one be collected.
Where LevelUpGo fits
LevelUpGo teaches Go through exercises that run real Go code in the browser. Composite Types covers maps next to slices and structs, including comma-ok lookups, counting, deletion and iterating in a stable order. Concurrency Fundamentals covers goroutines, WaitGroups and the mutexes you need once a map is shared between them. For the other 24 reserved words, see Go keywords: all 25 explained.
FAQ
Is map a keyword in Go?
Yes. map is one of Go's 25 reserved keywords, so you can't name a variable or function map. It appears in map types such as map[string]int, in map literals and as the argument to make.
Is a Go map ordered?
No. The spec leaves iteration order unspecified, and the runtime randomizes it on every range. To process keys in order, sort them with slices.Sorted(maps.Keys(m)). fmt.Println and json.Marshal already print map keys sorted.
Are Go maps thread-safe?
No. Concurrent reads are safe, but any write alongside another read or write can crash the program with fatal error: concurrent map writes. Protect the map with a sync.Mutex or sync.RWMutex, or use sync.Map for write-once caches and disjoint key sets.
How do you get the length of a map in Go?
Call len(m). It returns the number of entries and works on a nil map, where it returns 0. Maps don't support cap.
Does Go have a set type?
No. Use map[T]struct{}, which stores only keys, and check membership with _, ok := set[k]. map[T]bool also works if you prefer if set[k] over comma-ok.
What happens when you read a key that doesn't exist?
You get the zero value of the value type, such as 0, "", false or nil, and no error. Use the two-value form v, ok := m[k] when you need to tell a missing key from a stored zero value.
Sources
- 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
