Question
In Go, maps are reference types. What is the difference between initializing an empty map with make and using a map literal?
copyM := make(map[string]int32)
copyM := map[string]int32{}
Which form is generally preferable, and when should each be used?
Short Answer
You will learn how Go map initialization works, why both examples create writable empty maps, and when make, a map literal, or a nil map is the most appropriate choice.
Concept
A Go map is a reference-like data structure: a map variable refers to an underlying hash-table data structure managed by Go.
Both of these statements create an empty, non-nil, writable map:
copyM := make(map[string]int32)
copyM := map[string]int32{}
After either statement, you can safely assign entries:
copyM["books"] = 3
For an empty map with no special requirements, the two forms behave the same in normal use. The main differences are intent and features:
- Use
make(map[K]V)when you want to create an empty map, especially when supplying a capacity hint. - Use
map[K]V{}when you want literal syntax, especially when providing initial key-value entries.
A capacity hint can help when you expect to add many entries:
scores := make(map[string]int, 1_000)
The number is a hint, not a map length. len(scores) is still 0 immediately after creation.
Mental Model
Think of a map as a filing cabinet.
- A nil map is a label for a cabinet that has not been delivered yet. You can ask whether a file exists, but you cannot put a file into it.
make(map[string]int)is ordering and receiving an empty filing cabinet.map[string]int{}is also receiving an empty filing cabinet, but using literal notation.map[string]int{"Ada": 10}is receiving a cabinet that already contains labeled files.
The two empty-cabinet forms are equivalent for adding and reading entries. Choose the notation that makes your code's purpose clearest.
Syntax and Examples
Create an empty writable map with make:
counts := make(map[string]int)
counts["apple"] = 2
Create an empty writable map with a literal:
counts := map[string]int{}
counts["apple"] = 2
Create a map with initial entries using a literal:
prices := map[string]float64{
"coffee": 3.50,
"tea": 2.75,
}
Create a map with a capacity hint using make:
users := make(map[string]bool, 500)
The 500 does not insert 500 elements. It tells Go that the map may grow to around that size, which can reduce internal resizing work when many entries will be added.
A declared map without initialization is nil:
var counts map[string]int
fmt.Println(counts == nil) // true
fmt.Println(counts["apple"]) // 0: reading is safe
// counts["apple"] = 2 // panic: assignment to entry in nil map
Step by Step Execution
Consider this program:
package main
import "fmt"
func main() {
inventory := map[string]int{}
inventory["pen"] = 4
inventory["pen"]++
fmt.Println(inventory)
fmt.Println(len(inventory))
}
Execution flow:
map[string]int{}creates an empty, non-nil map whose keys arestringvalues and whose values areintvalues.inventory["pen"] = 4adds the key"pen"with value4.inventory["pen"]++reads the current value (4), adds one, and stores5for the same key.- Printing the map displays data equivalent to
map[pen:5]. Map display order should not be relied on when several entries exist. len(inventory)returns1, because the map has one key.
Replacing the first line with the following produces the same result:
inventory := make(map[string]int)
Real World Use Cases
-
Counting events: Store how many times each endpoint, product, or error code occurs.
visits := map[string]int{} visits["/pricing"]++ -
Building a lookup table: Associate user IDs with user records loaded from a database or API.
usersByID := make(map[string]User, len(users)) -
Removing duplicates: Use a map as a set, often with
struct{}values.seen := make(map[string]struct{}) seen["go"] = struct{}{} -
Configuration defaults: Use a map literal when the default values are known in source code.
limits := map[string]int{ "free": 100, "pro": 10_000, } -
JSON-like dynamic data: Build a
map[string]anybefore sending data to an API or encoding JSON.
Real Codebase Usage
In real Go projects, the form usually communicates intent.
Use a literal for fixed initial data
var statusText = map[int]string{
200: "OK",
404: "Not Found",
500: "Internal Server Error",
}
This is clear because the map's contents are part of the definition.
Use make while collecting data dynamically
func groupByTeam(members []Member) map[string][]Member {
groups := make(map[string][]Member)
for _, member := range members {
groups[member.Team] = append(groups[member.Team], member)
}
return groups
}
Supply a capacity hint for known input sizes
func indexByID(records []Record) map[string]Record {
index := make(map[string]Record, len(records))
for _, record := range records {
index[record.ID] = record
}
return index
}
This is a common indexing pattern. The hint is an optimization, not a correctness requirement.
Preserve nil when it has meaning
Some APIs use nil to mean “not supplied” and an empty map to mean “supplied, but empty.” Do this only when that distinction is meaningful to callers, serialization, or business rules.
Common Mistakes
Writing to a nil map
This compiles, but it panics at runtime:
var totals map[string]int
totals["order"] = 1 // panic: assignment to entry in nil map
Initialize it first:
totals := make(map[string]int)
totals["order"] = 1
Assuming make creates entries
scores := make(map[string]int, 100)
fmt.Println(len(scores)) // 0, not 100
The second argument is only a capacity hint.
Confusing reading with writing on a nil map
Reading is safe and returns the value type's zero value:
var flags map[string]bool
fmt.Println(flags["enabled"]) // false
Writing is not safe unless the map is initialized.
Forgetting that a missing key returns a zero value
ages := map[string]int{}
fmt.Println(ages["unknown"]) // 0
Use the comma-ok form when you must distinguish a missing key from a stored zero value:
age, found := ages["unknown"]
fmt.Println(age, found) // 0 false
Relying on map iteration order
Do not assume this loop has a predictable order:
for key, value := range ages {
fmt.Println(key, value)
}
If ordering matters, collect and sort the keys first.
Comparisons
| Form | Result | Can write entries? | Best use |
|---|---|---|---|
var m map[string]int | Nil map | No | Optional map that may remain absent |
make(map[string]int) | Empty non-nil map | Yes | Dynamically populated map |
make(map[string]int, n) | Empty non-nil map with capacity hint | Yes | Many expected insertions |
map[string]int{} | Empty non-nil map | Yes | Concise empty literal or consistency with literals |
map[string]int{"a": 1} | Non-nil map with entries | Yes | Known initial values |
make is used for maps, slices, and channels because these types need runtime initialization. A composite literal such as map[string]int{} is useful when declaring the map structure and, optionally, its starting contents in one expression.
For the original two forms, neither is universally “nicer.” A common convention is:
- prefer
map[K]V{}when a literal is natural or initial entries are nearby; - prefer
make(map[K]V)when the map will be filled programmatically; - prefer
make(map[K]V, len(input))when an appropriate capacity estimate is known.
Cheat Sheet
// Nil map: reads work; writes panic
var m map[string]int
// Empty, writable maps
m1 := make(map[string]int)
m2 := map[string]int{}
// Empty, writable map with capacity hint
m3 := make(map[string]int, 100)
// Map with initial entries
m4 := map[string]int{
"apples": 3,
"oranges": 5,
}
// Insert or update
m1["apples"] = 3
// Read; missing keys produce the zero value
count := m1["missing"] // 0
// Check whether a key exists
count, ok := m1["apples"]
// Delete a key
delete(m1, "apples")
Rules:
make(map[K]V)andmap[K]V{}both create writable empty maps.make(map[K]V, n)usesnas a capacity hint, not the length.- Only map literals can include initial key-value entries directly.
- A nil map can be read, ranged over, and passed to
len, but cannot receive assignments. - Map iteration order is not guaranteed.
FAQ
Is make(map[string]int) the same as map[string]int{} in Go?
For creating an empty map that you can read from and write to, yes. Both produce an empty, non-nil map.
Which map initialization style should I use in Go?
Use a literal when declaring known initial entries. Use make when creating an empty map to fill dynamically, especially when you have a useful capacity estimate.
Does make(map[string]int, 10) create 10 map entries?
No. It creates an empty map with a capacity hint for approximately 10 entries. Its length is still zero.
Why does assignment to a nil map panic?
A nil map has no initialized underlying storage for new entries. Initialize it with make or a map literal before assigning keys.
Can I read from a nil map in Go?
Yes. Reading a missing key from a nil map returns the zero value for the map's value type. Writing still panics.
Can I compare two maps with == in Go?
Maps cannot be compared to each other. A map can only be compared with nil, such as m == nil.
Does an empty map equal a nil map?
They are different states. An empty map is initialized and writable; a nil map is uninitialized. Both have length zero.
Mini Project
Description
Build a small word-frequency counter for a text string. It demonstrates how to initialize a map, add values dynamically, use a capacity hint, and check the result for a requested word.
Goal
Create a function that counts each word in a sentence and reports the count of a selected word.
Requirements
Initialize a map that stores words as keys and counts as integer values. Convert the input text to lowercase before counting. Count each word by updating its map entry. Print every word and its count. Use the comma-ok map lookup form to report whether a requested word was present.
Keep learning
Related questions
Are Go Maps Passed by Value or Reference?
Learn Go map parameter and return semantics: maps are passed by value, while copies share underlying data. Know when map pointers are useful.
Authentication for Go REST APIs: Sessions, Tokens, and Middleware
Learn practical authentication patterns for Go REST APIs, including password hashing, server sessions, bearer tokens, middleware, and security rules.
Automatic Build Versioning in Go: Embed Incrementing Build Numbers
Learn how to add automatic build versioning in Go using linker flags, build metadata, CI counters, and Git-based version values.