Go
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions and Methods
-
Section 4: Concurrency
-
Section 5: Packages and Tooling
-
Section 6: More Standard Library
-
Section 7: Building Services
-
Section 8: Advanced Go
-
Section 9: Go in the Cloud-Native Ecosystem
-
Section 10: Data Structures and Algorithms in Go
-
Section 11: Testing and Deployment
-
Section 12: Practical Projects
-
Section 13: More Standard Library Practice
-
Section 14: More Practice Projects
-
Section 15: Design Patterns in Go
-
Section 16: Interview Practice
-
Section 17: Package fmt In Depth
-
Section 18: Package strings and strconv
-
Section 19: Package os and io
-
Section 20: Package time
-
Section 21: Package sort and container
-
Section 22: Package encoding
-
Section 23: Package net/http In Depth
-
Section 24: Package context
-
Section 25: Package regexp and bytes
-
Section 26: Package errors In Depth
-
Section 27: Package crypto and hash
-
Section 28: Package flag and log
-
Section 29: Package sync In Depth
-
Section 30: More Practice Exercises
-
Section 31: Go Modules and Workspaces In Depth
-
Section 32: Generics Deep Dive (Go 1.18+)
-
Section 33: Testing Package In Depth
-
Section 34: More Interview and Whiteboard Practice
-
Section 35: Package math and unicode
-
Section 36: Package path and filepath
-
Section 37: Package database/sql
-
Section 38: Package text/template and html/template
-
Section 39: Package archive and compress
-
Section 40: Lower-Level net Package
-
Section 41: Package runtime and reflect
-
Section 42: Package embed and io/fs
-
Section 43: Go Assembly and CGO Basics
-
Section 44: Building CLIs and TUIs
-
Section 45: Go Performance Tuning
-
Section 46: More Real-World Projects
-
Section 47: Go in Production
-
Section 48: Go Design Patterns
-
Section 49: Go Interfaces Deep Dive
-
Section 50: Final Practice and Review
12: Slice Internals: Length and Capacity
I once spent an entire afternoon debugging a "ghost" bug with a junior dev who was writing a high-performance log parser. He was slicing a massive byte buffer into smaller chunks to process different log entries. He noticed that when he modified a specific chunk, the data in a completely different part of the program—which he thought was a separate slice—was also changing. He was convinced there was a memory leak or some weird pointer magic happening. In reality, he had just fallen into the classic trap of not understanding how length and capacity actually work in Go. He thought he was creating copies of the data, but he was actually just moving a window over the same underlying array.
The Slice Header: What's Actually Under the Hood
To get why that bug happened, you have to stop thinking of a slice as "a dynamic array" and start thinking of it as a header. When you declare a slice, Go creates a small struct behind the scenes that contains three things: a pointer to an underlying array, a length, and a capacity. That's it.
// This is conceptually what a slice looks like internally
type sliceHeader struct {
Data uintptr // Pointer to the underlying array
Len int // Current number of elements
Cap int // Total capacity of the underlying array from the start of the slice
}
The length is what you see when you call len(); it's the number of elements the slice currently holds. The capacity, which you get from cap(), is the total number of elements the underlying array can hold, starting from the first element of that slice. This distinction is critical because it determines whether adding a new element is a cheap operation or a heavy one.
The Dance Between Length and Capacity
Think of the capacity as the "room to grow." If you create a slice using make([]int, 3, 5), you're telling Go: "Give me a slice with 3 elements, but allocate an underlying array big enough to hold 5."
s := make([]int, 3, 5)
fmt.Println(len(s)) // 3
fmt.Println(cap(s)) // 5
// We can grow the length up to the capacity without allocating new memory
s = s[:5]
fmt.Println(len(s)) // 5
fmt.Println(cap(s)) // 5
Now, here is where the "ghost bug" usually happens. If you take a slice of an existing slice, the new slice shares the same underlying array. If you slice from the middle, your length decreases, but your capacity is still calculated from the start of the slice to the end of the original array. I've seen countless developers assume that s2 := s1[1:3] creates a new array. It doesn't. It just creates a new header pointing one element further into the same array. If you change s2[0], you are changing s1[1].
When Append Forces a Migration
The append() function is where the magic (and the performance cost) happens. When you append an element to a slice, Go checks if the current length is less than the capacity. If it is, Go just increments the length and writes the value into the existing underlying array. Fast and efficient.
But what happens when len == cap? Go can't just grow an array in place because the memory around it might be occupied by other data. Instead, Go performs a "migration." It allocates a new, larger array (usually double the size for smaller slices), copies all the existing elements over, and then updates the slice header to point to this new location.
s := make([]int, 2, 2) // Length 2, Capacity 2
s = append(s, 3) // Capacity is full!
// Go allocates a new array (likely cap 4), copies [0, 0], adds 3.
// 's' now points to a completely different memory location.
This is a vital detail. If you pass a slice to a function and that function appends to it, the function might trigger a migration. Since the slice header is passed by value, the original caller still has the old header pointing to the old array, while the function is now working with a new array. If you've ever wondered why your slice didn't "update" after being passed into a function, this is usually the culprit.
📋 Practical Task
Exercise: Fixing the Shared Buffer Corruption
You are reviewing code for a telemetry system. The developer tried to implement a "buffer splitting" logic to separate a header from a payload, but they've introduced a bug where modifying the payload accidentally corrupts the header because they share the same underlying array.
Your Goal: Modify the splitBuffer function so that the payload slice has its own independent underlying array, ensuring that changes to the payload do not affect the original buffer or the header.
package main
import "fmt"
func splitBuffer(buffer []byte) ([]byte, []byte) {
// BUG: This creates a slice that points to the same underlying array as buffer
header := buffer[:2]
payload := buffer[2:]
return header, payload
}
func main() {
// A sample buffer: [Header1, Header2, Data1, Data2]
buffer := []byte{0xAA, 0xBB, 0x01, 0x02}
header, payload := splitBuffer(buffer)
// The developer wants to normalize the payload by zeroing it out
// but this currently changes the original 'buffer' and 'header' too!
for i := range payload {
payload[i] = 0x00
}
fmt.Printf("Buffer: %x\n", buffer)
fmt.Printf("Header: %x\n", header)
fmt.Printf("Payload: %x\n", payload)
// EXPECTED OUTPUT:
// Buffer: aabb0102 (Original should remain untouched)
// Header: aabb
// Payload: 0000
}There are no comments for now.