Demystifying GO slices Length vs Capacity 🚌
Posted on 2025/07/27 09:20:10
If you're working with Go, you've undoubtedly encountered slices. They are one of the language's most powerful and common data structures. However, the relationship between a slice's length and it's capacity it can be crazy thing to understand at first, especially when using the append function, things get even crazier what hell is happening???
To make this clear, let's use a simple analogy.
The Bus Analogy
Think of a Go slice as a bus:
Length (len): This is the number of people currently on the bus. It's how many elements the slice is actively using.
Capacity (cap): This is the total number of seats available on the bus. It represents the size of the underlying array that backs the slice.
You can add more people (append elements) as long as there are empty seats. When the bus is full (len == cap), adding another person requires getting a bigger bus. In Go, this means allocating a new, larger underlying array and copying the existing elements over.
Slicing a slice the [start:end] rule
To create a new slice from an existing one, you use the aSlice[start:end] syntax. The rule is simple but crucial:
The new slice includes the element at the start index but excludes the element at the end index.
Visual example
Imagine an array with the following values and indices:
Index: 0 1 2 3 4
Value: │ 10│ 20│ 30│ 40│ 50│
If we slice it using [1:3]:
- We start at index 1 and take its value, 20. ✅
- We continue to index 2 and take its value, 30. ✅
- We stop when we reach the end index 3. We do not include the value at this index. 🛑
The resulting slice is [20, 30].
Shorthand slicing
aSlice[2:]is shorthand for "from index 2 to the very end."aSlice[:3]is shorthand for "from the beginning up to (but not including) index 3."
Let's put all together
This program demonstrates how length, capacity, and appending work in practice.
package main
import "fmt"
func main() {
// 1. Create a slice. The bus has 5 people and 5 seats.
allSpots := []int{10, 20, 30, 40, 50}
fmt.Printf("1. allSpots -> len: %d, cap: %d, data: %v\n", len(allSpots), cap(allSpots), allSpots)
// 2. Create a new slice viewing a part of the original.
// We are *using* 2 spots (len=2), but our "view" can see 4 total seats (cap=4).
mySpots := allSpots[1:3] // Elements at index 1 and 2
fmt.Printf("2. mySpots -> len: %d, cap: %d, data: %v\n", len(mySpots), cap(mySpots), mySpots)
// 3. Append an element while still within capacity.
// This uses an empty seat on the *original* bus.
mySpots = append(mySpots, 99)
fmt.Printf("3. mySpots (appended) -> len: %d, cap: %d, data: %v\n", len(mySpots), cap(mySpots), mySpots)
fmt.Printf(" allSpots is now changed! -> data: %v\n", allSpots)
// 4. Append more elements, exceeding capacity.
// Go gets a bigger bus (a new array), leaving the original behind.
mySpots = append(mySpots, 88, 77)
fmt.Printf("4. mySpots (new array) -> len: %d, cap: %d, data: %v\n", len(mySpots), cap(mySpots), mySpots)
fmt.Printf(" allSpots is NOT changed -> data: %v\n", allSpots)
}
Code output breakdown of what's happening
1. The Initial Slice
allSpots -> len: 5, cap: 5, data: [10 20 30 40 50]
We create a slice with 5 elements. Since we didn't slice it from a larger array, its length and capacity are both 5.
2. Creating a "View" with a New Slice
mySpots -> len: 2, cap: 4, data: [20 30]
Length is 2 because the slice expression [1:3] selects two elements (at index 1 and 2). Capacity is 4 because the new slice's "view" of the underlying array starts at index 1 and goes to the end. There are 4 elements from that point onward (20, 30, 40, 50).
3. Appending Within Capacity
mySpots (appended) -> len: 3, cap: 4, data: [20 30 99]
allSpots is now changed! -> data: [10 20 30 99 50]
We appended 99 to mySpots. Since its len (2) was less than its cap (4), there was room. The length grew to 3. Most importantly, allSpots was also modified! This is the critical "gotcha." Because mySpots still had available capacity in the shared underlying array, the append operation placed the new value 99 into that array, overwriting the original value 40.
4. Appending Beyond Capacity (The Big Move!)
mySpots (new array) -> len: 5, cap: 8, data: [20 30 99 88 77]
allSpots is NOT changed -> data: [10 20 30 99 50]
Here, we tried to add two more elements, but mySpots only had one spot of capacity left (cap 4 - len 3 = 1). Go needed to find a bigger bus. It allocated a brand new, larger array, copied the elements from mySpots ([20 30 99]) into it, and then added the new elements (88, 77). The new capacity is 8 (Go usually doubles the capacity). Because mySpots now points to a completely different array, the original allSpots is no longer affected and remains unchanged from the previous step. The link between them is broken.
Key Takeaways
This "reallocation" is the most important concept to grasp. As long as a sub slice has room within its capacity, it's merely a different "view" of the same underlying data like different groups of people on the same bus. Your actions can affect everyone else on board.
The moment you append beyond the capacity, Go is forced to get a bigger bus. From that point on, your slice's journey is completely separate from the original.
The rule of thumb for safe slicing is to avoid subtle bugs, always ask yourself: "Will this append fit, or will it force a reallocation?"
If you need to guarantee that a new slice can be modified without any risk of affecting the original, the best practice is to create an explicit, independent copy from the start.
// To create a truly independent slice from the beginning:
original := []int{10, 20, 30, 40, 50}
subSlice := original[1:3] // [20, 30]
// Create a new slice with its own underlying array
independentCopy := make([]int, len(subSlice))
copy(independentCopy, subSlice)
// Now, appending to independentCopy will never affect the original slice.
independentCopy = append(independentCopy, 99)
fmt.Println(original) // Prints: [10 20 30 40 50] (un-changed!)
By mastering the interplay between length, capacity, and the underlying array, you've turned a potential source of confusion into a powerful tool. You can now wield slices with confidence, writing more efficient and predictable Go code. Happy coding!