Slice Declaration
Unlike languages such as PHP, Go provides slices as a reference type that wraps an underlying array. Arrays in Go have fixed sizes, but slices offer dynamic behavior while referencing array elements.
/*
* Array declaration
*/
var a [5]int // Length specified, elements default to 0
var a [5]int{1, 2, 3, 4, 5}
/*
* Slice declaration - like declaring an array without length
*/
// No underlying array created
// Method 1: Direct initialization
var s []int // nil slice with length and capacity of 0
var s []int{1, 2, 3, 4, 5} // Creates a 5-element array
// Method 2: Using make() function
// var variableName = make([]elementType, length, capacity)
var s = make([]int, 0, 5)
// Underlying array already exists
// Slice with range: var variableName []elementType = arr[low, high]
var arr = [5]int{1, 2, 3, 4, 5}
var slice []int = arr[1:4] // Elements at indices 1, 2, 3: [2, 3, 4]
Length vs Capacity
The length of a slice represents the number of elements it currently contains. The capacity indicates the number of elements from the first element to the end of the underlying array. Use len() and cap() to retrieve these values:
s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} // [0 1 2 3 4 5 6 7 8 9] len=10, cap=10
s1 := s[0:5] // [0 1 2 3 4] len=5, cap=10
s2 := s[5:] // [5 6 7 8 9] len=5, cap=5
How append Modifies Length and Capacity
The append Function
Go provides the built-in append function to add elements to a slice:
func append(s []T, vs ...T) []T
The return value is a slice containing all original elements plus the new ones.
Example 1: Capacity remains unchanged
package main
import "fmt"
func main() {
arr := [5]int{1, 2, 3, 4, 5}
fmt.Println(arr)
s1 := arr[0:3] // [1 2 3]
printDetails(s1)
s1 = append(s1, 6)
printDetails(s1)
fmt.Println(arr)
}
func printDetails(s []int) {
fmt.Printf("len=%d cap=%d %p %v\n", len(s), cap(s), s, s)
}
Output:
[1 2 3 4 5]
len=3 cap=5 0xc000082030 [1 2 3]
len=4 cap=5 0xc000082030 [1 2 3 6]
[1 2 3 6 5]
After appending, the slice's capacity and memory address remain unchanged. The underlying array reflects the modification at index 3, where 4 became 6.
Example 2: Capacity doubles
package main
import "fmt"
func main() {
arr := [5]int{1, 2, 3, 4}
fmt.Println(arr)
s2 := arr[2:] // [3 4 0]
printDetails(s2)
s2 = append(s2, 5)
printDetails(s2)
fmt.Println(arr)
}
func printDetails(s []int) {
fmt.Printf("len=%d cap=%d %p %v\n", len(s), cap(s), s, s)
}
Output:
[1 2 3 4 0]
len=3 cap=3 0xc00001c130 [3 4 0]
len=4 cap=6 0xc00001c180 [3 4 0 5]
[1 2 3 4 0]
Here, both capacity and memory address changed, while the original array stayed intact.
When the underlying array lacks sufficient capacity, Go allocates a larger array and returns a slice pointing to this new allocation.
Internal Slice Structure
The slice data structure is defined in src/runtime/slice.go:
// go 1.3.16 src/runtime/slice.go:13
type slice struct {
array unsafe.Pointer
len int
cap int
}
A slice is a struct containing the length, capacity, and a pointer to the underlying array. When appending exceeds capacity, the growslice function handles allocation:
// go 1.3.16 src/runtime/slice.go:76
func growslice(et *_type, old slice, cap int) slice {
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old.len < 1024 {
newcap = doublecap
} else {
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
if newcap <= 0 {
newcap = cap
}
}
}
var overflow bool
var lenmem, newlenmem, capmem uintptr
switch {
// ...code
}
memmove(p, old.array, lenmem)
return slice{p, old.len, newcap}
}
The growth rules are:
- If capacity is under 1024 elements, it doubles on each growth
- Once capacity exceeds 1024, growth factor becomes 1.25 (adding 25% per expansion)
- If expansion stays within the original array's bounds, the slice still references the original array
- If expansion exceeds the original array's capacity, a new array is allocated and data is copied—the original array remains unaffected
In both examples above, capacity was under 1024, so the growth factor was 2. In Example 2, the slice's underlying array lacked available space, so append() created a new array, copied existing values, and added the new one, leaving the original array untouched.
Visualizing Growth Scenarios
Scenario 1: Capacity unchanged after growth
slice := []int{1, 2, 3, 4, 5}
mySlice := slice[1:3] // Length: 2, Capacity: 4
mySlice = append(mySlice, 40)
The resulting memory layout shows the slice utilizing existing capacity without triggering reallocation.
Scenario 2: New allocation after growth
mySlice := []int{1, 2, 3, 4, 5} // Length and capacity both 5
mySlice = append(mySlice, 6)
This triggers a new array allocation since the original capacity was exhausted.
Key Takeaways
- A slice is a struct containing capacity, length, and a pointer to an underlying array
- Setting initial capacity avoids
growslicecalls—new allocations create new memory addresses and copy data, impacting performance