Series Intro:
Welcome to the start of my Go maps deep-dive series!
In the upcoming parts, we’ll go from the outer layer of maps to their deep internals — how they’re implemented under the hood, memory layout, hash functions, optimizations, and performance tricks.
This is Part 0, where we’ll start from the basics: what maps are, how they behave, and what you should know before diving deeper.
🧩 What Are Maps?
In Go, a map is a key-value data structure.
If you’ve used Rust, you know it as a HashMap.
If you’ve used Python, you know it as a dict.
m := map[string]int{
"apple": 5,
"orange": 3,
}
fmt.Println(m["apple"]) // 5
They’re unordered collections of key-value pairs, designed for fast lookups and inserts.
🗝️ Maps Are Pointers
Unlike slices, maps in Go are internally pointers to a data structure called hmap.
This has two important consequences:
- You don’t need to pass them by pointer.
- When you pass a map to a function, you’re already passing a pointer under the hood. ( in old versions you were used to do like *map[int]int but changed) see Lan Taylor
In the very early days what we call maps now were written as pointers, so you wrote *map[int]int. We moved away from that when we realized that no one ever wrote
map
without writing*map
.
The zero value is nil.
var m map[string]int
fmt.Println(m == nil) // true
- if you declare a map and then assign it to the new value the both m1, and m2 will be placed in unique memory locations (see blog by dave chenny There is no pass-by-reference in Go) and them value both will be the address of a same hmap structure.
- Actually the m1 will be cpoied to the m2 (like all assigns in Golang)
- Updating m2 also updates m1.
m1 := make(map[string]int)
m2 := m1
Diagram: m1
and m2
point to the same underlying hmap
struct ( picture by VictoriaMetrics )
⚖️ Maps Are Not Comparable
Unlike slices, you cannot compare two maps directly:
m1 := map[string]int{}
m2 := map[string]int{}
// This will not compile ❌
if m1 == m2 {}
// This will not compile too ❌
if m1 == m1 {}
// But you *can* check if a map is nil ✅
if m1 == nil {
fmt.Println("m1 is nil")
}
🔑 Keys Must Be Comparable
The type you use as a key must be comparable in Go.
✅ Valid keys:
- string
- int, float64
- struct (if all fields are comparable)
❌ Invalid keys:
- slice
- map
- function
interface{} or any can be sometimes ok sometimes nok:
var x, x2 map[interface{}]int
x["a"] = 1 // OK✅
x2[[]int{1, 2}] = 1 //runtime panic!❌
⏱️ Lookup and Insert Are Usually O(1)
Go maps are hash-based, so operations like searching and inserting are on average O(1):
Why “usually” O(1)?
In upcoming parts, we’ll explore hash collisions, buckets, and why performance can sometimes degrade. Stay tuned! 👀
⚡ Maps Are Not Thread-Safe
Go maps are not safe for concurrent writes.
If you try, you’ll get a fatal runtime error:
m := map[int]int{}
go func() { m[1] = 42 }()
go func() { m[2] = 84 }()
💥 Runtime panic: concurrent map writes
🏗️ Creating Maps with make
You can create maps with make:
m := make(map[int]int, 10)
Here, the 10 is not the length — it’s a hint to Go about the expected size.
This helps reduce resizing overhead when inserting many items.
We’ll talk about why it’s just a hint and how Go actually allocates buckets in a future part.
🔜 What’s Next?
This is just the beginning! In Part 1, we’ll start peeling back the layers of the hmap structure — the backbone of Go maps.
We’ll cover:
- The internal hmap struct
- Buckets and hashing
- Load factors and resizing strategies
and after all parts you will have good sight about what happening exactly inside the go internals for maps
Stay tuned — things are about to get really interesting! 🚀
Top comments (0)