How to Create a Dictionary in Go: Complete Guide with Examples - comprehensive 2026 data and analysis

How to Create a Dictionary in Go: Complete Guide with Examples

Last verified: April 2026

Executive Summary

Go doesn’t have a built-in “dictionary” type like Python or JavaScript—instead, it uses maps as its key-value data structure. Maps are one of Go’s most versatile features, allowing you to store and retrieve data with O(1) average lookup time. This guide covers the practical techniques for creating, initializing, and working with maps effectively in production Go code.

Learn Go on Udemy


View on Udemy →

Main Data Table

Map Declaration Method Syntax When to Use Memory Overhead
Empty declaration var m map[string]int When nil map is acceptable Minimal (nil pointer)
Literal initialization m := map[string]int{"a": 1} Known values at compile time Allocates immediately
Make with capacity m := make(map[string]int, 100) Predictable size, optimal performance Pre-allocated buckets
Make without capacity m := make(map[string]int) Dynamic sizing, unknown capacity Grows as needed

Breakdown by Experience Level

Map creation difficulty varies based on your Go experience:

Experience Level Recommended Approach Complexity Rating
Beginner Literal initialization or make() Easy
Intermediate Make with capacity planning Intermediate
Advanced Custom hashing, sync.Map for concurrency Advanced

How to Create Dictionaries in Go: Practical Examples

1. Basic Map Declaration

The simplest way to create a map in Go is using variable declaration:

package main

import "fmt"

func main() {
    // Declare a map but don't initialize it
    var ages map[string]int
    
    // This will print: map[]
    fmt.Println(ages)
    
    // WARNING: Assigning to a nil map will panic!
    // ages["Alice"] = 30  // This causes a runtime panic
}

Important: A declared but uninitialized map is nil. You cannot write to a nil map—it will panic at runtime. Always initialize your maps before use.

2. Initialize with make()

The most idiomatic way to create a usable map:

package main

import "fmt"

func main() {
    // Create an empty map with no initial capacity
    users := make(map[string]string)
    users["alice"] = "Alice Smith"
    users["bob"] = "Bob Jones"
    
    // Create a map with initial capacity (recommended for known sizes)
    // This pre-allocates internal hash buckets
    cache := make(map[string]interface{}, 1000)
    
    fmt.Println("Users:", users)
    fmt.Println("Cache capacity hint:", len(cache))  // Still 0, capacity is internal
}

Best practice: Use make() with a capacity hint when you know roughly how many entries you’ll store. This reduces internal resizing operations and improves performance by 20-30% in benchmarks.

3. Literal Initialization

When you have values ready at compile time, use map literals:

package main

import "fmt"

func main() {
    // Map literal with initial values
    config := map[string]string{
        "host":     "localhost",
        "port":     "8080",
        "database": "mydb",
    }
    
    // Nested maps
    database := map[string]map[string]int{
        "products": {
            "count":    150,
            "active":   120,
        },
        "users": {
            "count":    5000,
            "active":   3200,
        },
    }
    
    fmt.Println("Config:", config)
    fmt.Println("Active products:", database["products"]["active"])
}

4. Working with Different Key and Value Types

package main

import "fmt"

func main() {
    // String keys, integer values
    scores := map[string]int{
        "alice": 95,
        "bob":   87,
    }
    
    // Integer keys, string values
    codes := map[int]string{
        200: "OK",
        404: "Not Found",
        500: "Internal Server Error",
    }
    
    // String keys, slice values
    teams := map[string][]string{
        "frontend": {"alice", "charlie"},
        "backend":  {"bob", "diana"},
    }
    
    // String keys, struct values
    type User struct {
        Name  string
        Email string
        Age   int
    }
    
    users := map[string]User{
        "alice": {"Alice Smith", "alice@example.com", 28},
        "bob":   {"Bob Jones", "bob@example.com", 34},
    }
    
    fmt.Println(scores["alice"])          // 95
    fmt.Println(codes[404])                // Not Found
    fmt.Println(teams["frontend"])        // [alice charlie]
    fmt.Println(users["alice"].Email)     // alice@example.com
}

5. Common Operations on Maps

package main

import "fmt"

func main() {
    m := make(map[string]int)
    
    // Adding/updating values
    m["apple"] = 5
    m["banana"] = 3
    
    // Reading with comma-ok idiom to check existence
    if price, exists := m["apple"]; exists {
        fmt.Printf("Apple costs %d\n", price)
    }
    
    // Reading non-existent key returns zero value
    fmt.Println(m["orange"])  // 0
    
    // Deleting entries
    delete(m, "banana")
    
    // Iterating over maps (order is random)
    for fruit, price := range m {
        fmt.Printf("%s: %d\n", fruit, price)
    }
    
    // Checking length
    fmt.Println("Map has", len(m), "entries")
}

Comparison with Alternative Data Structures

Data Structure Use Case Lookup Time Pros Cons
Map (hash table) General key-value storage O(1) average Fast lookups, flexible keys Unordered, not thread-safe
Slice of structs Ordered collections O(n) linear search Ordered, can be sorted Slower lookups, more memory
sync.Map Concurrent access O(1) amortized Thread-safe, no locks needed Lower performance than map in single-threaded code
Custom B-tree Sorted key access O(log n) Sorted iteration, range queries Higher memory overhead, slower inserts

Key Factors to Consider When Creating Maps

1. Nil vs Empty Map

A crucial distinction in Go: a nil map and an empty map behave differently. A nil map cannot be written to, but an empty map (created with make()) can. Always initialize maps before assignment, especially when returning maps from functions or storing them in structs. This prevents runtime panics and makes your code more robust.

2. Capacity Planning

Pre-allocating capacity with make(map[string]int, 1000) reduces the number of internal bucket resizing operations. When Go maps grow beyond their initial capacity, they rehash all entries into larger buckets—this is expensive. If you know you’ll insert roughly 1,000 entries, specifying that capacity upfront improves performance by 20-30% compared to growing organically.

3. Key Type Constraints

Not all types can be map keys. Go only allows types that support the == comparison operator: integers, floats, strings, booleans, arrays, and structs containing only comparable types. Slices, maps, and functions cannot be keys. This is a common source of compilation errors for newcomers.

4. Thread Safety

Standard maps in Go are not thread-safe. Concurrent reads and writes cause data races and undefined behavior. For multi-threaded applications, either protect map access with a mutex (sync.RWMutex) or use sync.Map for high-concurrency scenarios. sync.Map is optimized for cases where keys are mostly stable and reads dominate writes.

5. Memory Efficiency

Maps store key-value pairs in dynamically allocated buckets. Empty maps still consume memory for the map header (typically 48 bytes). If you’re creating thousands of maps, consider whether a slice of structs or a single shared map might be more efficient. Also, remember that map iteration order is randomized for security reasons—don’t rely on insertion order.

Historical Trends and Evolution

Go’s map implementation has evolved significantly since Go 1.0. Early versions used a simpler hash algorithm vulnerable to collision attacks. Go 1.6 introduced randomized map iteration order to prevent hash flooding denial-of-service attacks. Go 1.9 brought the sync.Map type specifically for concurrent workloads. Today, Go’s map implementation uses a sophisticated algorithm with multiple hash functions and optimized bucket structures, consistently delivering O(1) average lookup performance.

The introduction of generics in Go 1.18 hasn’t changed map syntax, but it enables type-safe wrapper functions around maps. This allows developers to create strongly-typed map abstractions without runtime overhead.

Expert Tips for Creating Maps

1. Always use make() with a capacity hint for large maps: Specify the expected size to avoid repeated resizing. m := make(map[string]int, 10000) is always better than growing organically if you know the final size.

2. Prefer the comma-ok idiom for safe reads: Always check if a key exists using value, ok := m[key] rather than assuming the key is present. This prevents subtle bugs from zero values masking missing entries.

3. Use sync.Map for concurrent access: Don’t protect maps with mutexes unless necessary. sync.Map is purpose-built for high-concurrency read-heavy workloads and requires no manual locking.

4. Create map wrappers for domain-specific types: Instead of exposing raw maps throughout your codebase, wrap them in custom types with methods. This enables easier refactoring and clearer intent.

5. Be aware of the randomized iteration order: Never assume maps iterate in a predictable sequence. If you need ordered iteration, maintain a separate slice of keys and sort it, or use a custom ordered map implementation.

FAQ Section

Q1: Can I use a slice as a map key?

No. Go only allows comparable types as map keys, and slices are not comparable (they can’t use the == operator reliably). If you need to use slice-like data as a key, convert it to a string, use a struct, or create a custom comparable type. For example: m := make(map[[3]int]string) works because arrays are comparable, but m := make(map[[]int]string) will fail to compile.

Q2: What happens when I iterate over a map?

Map iteration order is deliberately randomized in Go for security reasons. Every time you iterate with for k, v := range m, the order changes. This prevents hash flooding attacks but means you cannot rely on iteration order. If you need deterministic ordering, maintain a sorted slice of keys separately and iterate through that.

Q3: How do I safely access maps across multiple goroutines?

Use one of three approaches: (1) Protect the map with sync.RWMutex for general cases, (2) use sync.Map for high-concurrency read-heavy workloads, or (3) redesign to avoid shared mutable state entirely. The best approach depends on your read/write ratio—sync.Map excels when reads vastly outnumber writes.

Q4: Should I always pre-allocate map capacity?

Pre-allocate capacity when you have a reliable estimate of final map size, especially if it’s more than a few hundred entries. For small maps or when size is genuinely unknown, the overhead isn’t worth the complexity. Benchmarking your specific use case is always better than guessing.

Q5: Can I use a map in a struct and have it be safe to copy?

Yes, but with a caveat. Maps are reference types, so copying a struct containing a map creates a shallow copy pointing to the same underlying data. Modifying the map through either copy affects both. This is usually intentional, but be aware that struct copy doesn’t deeply copy maps. Use pointers if you need true independence: type Config struct { Cache *map[string]int }.

Conclusion

Creating dictionaries in Go means understanding maps—the language’s powerful built-in key-value data structure. Start with make(map[string]int) for simple cases, upgrade to capacity-hinted initialization for performance-critical code, and reach for sync.Map when concurrency enters the picture. Remember the three cardinal rules: always initialize before writing, use the comma-ok idiom for safe reads, and don’t assume iteration order.

Maps are Go’s answer to Python dictionaries and JavaScript objects, but they require slightly more intentionality around initialization and thread-safety. Write defensive code by checking key existence, pre-allocate when possible, and leverage Go’s standard library types like sync.Map for production systems. Master these patterns and you’ll have solid foundations for building scalable Go applications.

Learn Go on Udemy

Related: How to Create Event Loop in Python: Complete Guide with Exam


Related tool: Try our free calculator

Similar Posts