Data Structures - Arrays, Slices, Maps

The Memory Leak That Cost Us $2,000

Month two with Go in production. Our log aggregation service was consuming 12GB of RAM and climbing. Every hour, memory usage increased by 500MB. After three days, the container OOMKilled and restarted.

I found the culprit:

var allLogs []string  // Growing forever!

func processLogBatch(logs []string) {
    allLogs = append(allLogs, logs...)  // Memory leak!
    // Process logs
    sendToElasticsearch(allLogs)
}

I was appending every batch to a global slice that never cleared. After processing millions of log lines, we were holding everything in memory.

The fix was understanding slices:

func processLogBatch(logs []string) {
    // Process only current batch
    sendToElasticsearch(logs)
    // logs goes out of scope, garbage collected
}

Memory usage dropped to 200MB stable. This article covers the slice internals I wish I'd understood on day one.


Arrays: Fixed-Size Collections

Arrays in Go have fixed size determined at compile time.

Array Declaration

Arrays Are Values

Key point: Arrays are copied by value, not by reference.

Array Limitations

Reality: I almost never use arrays directly. Slices are better for 99% of cases.


Slices: Dynamic Arrays

Slices are Go's most-used data structure. They're references to underlying arrays with dynamic size.

Slice Declaration

Slice Internals

A slice has three components:

Visual representation:

Length vs Capacity


Append: Growing Slices

Basic Append

Must assign back! append returns a new slice.

Append Multiple Values

Capacity Growth

This is where the memory leak started:

When capacity is exceeded, Go allocates a new array (usually double the size) and copies elements.

Pre-allocate When Possible

This saved 40% memory allocations in my data processor.


Slicing Slices

Create Sub-slices

Critical: Sub-slices share the underlying array!

The Leak I Had


Copy Function

Partial Copy

Copy Overlapping Slices


Maps: Key-Value Pairs

Map Declaration

Map Operations

Zero Value for Missing Keys

Iterate Over Map

Important: Map iteration order is randomized. Don't rely on order!


Real-World Example: Word Counter


Map of Slices

Common pattern for grouping:


Maps Are Reference Types

To copy a map:


Practical Example: Cache Implementation

From my API service:


Comparing Slices and Maps

You cannot use == to compare slices or maps:

For maps:

Or use reflect.DeepEqual (slower):


Common Pitfalls

1. Forgetting to Assign Append

2. Range Loop Variable Reuse

3. Modifying Map While Iterating


Your Challenge

Implement a simple in-memory database:


Key Takeaways

  1. Arrays are fixed size, slices are dynamic

  2. Slices are references to underlying arrays

  3. Append must be assigned back: s = append(s, v)

  4. Pre-allocate slices when size is known for performance

  5. Sub-slices share memory - can cause leaks

  6. Maps are unordered and reference types

  7. Check map existence with two-value assignment

  8. Zero values: nil for slices/maps, 0 for missing map keys


What I Learned

The $2,000 memory leak taught me:

  • Slices grow automatically but never shrink

  • Understand capacity to avoid unnecessary allocations

  • Copy when needed to break references

  • Maps are perfect for fast lookups but use memory

Coming from Python's lists and dicts, Go's slices and maps felt similar. But the memory model is different - understanding references vs values prevented production bugs.


Next: Structs and Interfaces

In the next article, we'll explore Go's approach to types without classes: structs for data, interfaces for behavior, and composition over inheritance. You'll see how this simplifies design.

Last updated