10 Arrays and Slices in Go
Main topics:
- slicing
- slice literals
- reslicing and trimming
- slices for pass by reference of array
1. What is a slice?
-
A slice is a special type of array, that grows automatically (under the hood) when new lement is added beyond the maximum size (i.e. capacity)
-
If you are familiar with concept of vectors in C++, then you can relate this idea.
Basic syntax
- slices are declared with empty brackets []
- Arrays are declared three dots (...) or with a number in square brackets
- i.e. In Go, [N]T or [...]T defines an array (fixed size), while []T defines a slice (dynamic size)
// Array of 3 integers
var a [3]int // Array of 3 integers
// Slice of integers (length unspecified)
var s []int //[] instead of [3]
//π Arrays are with three dots (...) or with a number in square brackets
nums := []int{10, 20, 30} // β
Slice literal
nums := [3]int{10, 20, 30} // β
Array with fixed length
nums := [...]int{10, 20, 30} // β
Array with inferred length
Slice literal and slices from an array
Slices are created either of the following two ways:
- A slice literal is a compact syntax for creating a slice and initializing its elements in one step.
- Slice from Array (i.e. slicing an array):
package main
import "fmt"
func main() {
// Create a slice using a slice literal
nums := []int{10, 20, 30}
// Print the slice
fmt.Println(nums) // Output: [10 20 30]
fmt.Println(len(nums)) // Output: 3
fmt.Println(cap(nums)) // Output: 3
// Access and update an element
nums[1] = 99
fmt.Println(nums) // Output: [10 99 30]
// Append a new element
nums = append(nums, 40)
fmt.Println(nums) // Output: [10 99 30 40]
fmt.Println(len(nums)) // Output: 4 (elements)
fmt.Println(cap(nums)) // Output: 6 (maximum capacity increased from 3 to 6)
}
package main
import "fmt"
func main() {
// Create an array
arr := [5]int{10, 20, 30, 40, 50}
// Create a slice referencing part of the array
s := arr[1:4] // slice contains elements 20, 30, 40
fmt.Println("Original array:", arr) // [10 20 30 40 50]
fmt.Println("Slice s:", s) //[20 30 40]
fmt.Println("Slice length = ", len(s)) //3
fmt.Println("Slice cap= ", cap(s)) //4 (capacity = elements from starting index (i.e. 1) to end)
// Modify an element in the slice
s[1] = 99 // modifies the element at index 2 in the array
fmt.Println("Modified slice:", s)
fmt.Println("Array after slice modification:", arr)
fmt.Println("Original array (after modificatio of slice):", arr) // [10 20 99 40 50]
Original array: [10 20 30 40 50]
Slice s: [20 30 40]
Slice length = 3
Slice cap= 4
Modified slice: [20 99 40]
Array after slice modification: [10 20 99 40 50]
Original array (after modificatio of slice): [10 20 99 40 50]
slice is actually a pointer to an array with extra information like length and capacity
3. Slice internals: three elements
The Three Elements of a Slice
- Pointer: points to the first element of the underlying array that the slice references.
- Length (len): the number of elements the slice currently holds β what you get from len(slice).
- Capacity (cap): the maximum number of elements the slice can grow to within the underlying array before needing a new allocation β what you get from cap(slice).
append()
- append is only applicable to slices, NOT arrays.
- It adds elements to the end of a slice, possibly increasing its capacity.
4. How much capacity increases
When you append an element and capacity is exceeded, Go allocates a new underlying array with larger capacity.
-
The growth strategy depends on the current capacity:
- for small slices (capacity < 1024 elements), capacity roughly doubles.
- for large slices (capacity β₯ 1024), capacity grows by about 25% each time.
Note: In the example in previous section, the capacity increased from 3 to 6 (i.e. doubled) when a new element was added after exceeding the capacity
5. What happens when capacity exceeds
When you append to a slice and its length reaches its capacity, Go allocates a new, bigger underlying array.
- The existing elements are copied to this new array, and the slice now points to this new array.
- The original array remains unchanged because the slice no longer references it.
Example:
package main
import "fmt"
func main() {
// Original array
arr := [3]int{10, 20, 30}
// Create a slice referencing the array
s := arr[:]
fmt.Println("Before append:")
fmt.Println("Slice s:", s) // [10 20 30]
fmt.Println("Array arr:", arr) // [10 20 30]
fmt.Println("len:", len(s), "cap:", cap(s)) // len:3 cap:3
// Append element beyond capacity
s = append(s, 40)
fmt.Println("\nAfter append:")
fmt.Println("Slice s:", s) // [10 20 30 40]
fmt.Println("original Array arr:", arr) // [10 20 30] (unchanged)
fmt.Println("slice len:", len(s), "cap:", cap(s)) // len:4 cap:6 (capacity grows)
// Edit the first lement
s[1] = 99 //this element is within the range of original array length
fmt.Println("\nAfter Editing:")
fmt.Println("Slice s:", s) // [10 20 30 40]
fmt.Println("original Array arr:", arr) // [10 20 30] (unchanged)---hence once exceeded the original array stops reflecting the slice (but before exceeding it reflects slice)
fmt.Println("slice len:", len(s), "cap:", cap(s)) // len:4 cap:6 (capacity grows)
}
Before append:
Slice s: [10 20 30]
Array arr: [10 20 30]
len: 3 cap: 3
After append:
Slice s: [10 20 30 40]
original Array arr: [10 20 30]
slice len: 4 cap: 6
After Editing:
Slice s: [10 99 30 40]
original Array arr: [10 20 30]
slice len: 4 cap: 6
Sharing underlying array
As shown in example above:
- When you create a slice from an array or another slice, the new slice shares the same underlying array. This means:
- Changes made through the slice will reflect in the original array (or other slices from the same array).
- This saves memory and is efficient, but can lead to unintended side effects if you're not careful.
Example:
package main
import "fmt"
func main() {
arr := [5]int{10, 20, 30, 40, 50}
s1 := arr[1:4] // s1 = [20 30 40]
s2 := s1[1:] // s2 = [30 40]
s2[0] = 999 // modifies arr[2]
fmt.Println("Array:", arr) // [10 20 999 40 50]
fmt.Println("s1:", s1) // [20 999 40]
fmt.Println("s2:", s2) // [999 40]
}
- s1 and s2 both reference the same original array (arr).
- A change through s2 modified the array and was visible through s1 as well.
6. Reslicing
What is Reslicing?
- Reslicing means creating a new slice from an existing slice by slicing it again.
- We are creating a view/window over a portion of the original sliceβs elements.
- Reslicing does not copy data; the new slice still points to the same underlying array.
package main
import "fmt"
func main() {
s := []int{10, 20, 30, 40, 50}
// Original slice
fmt.Println("Original slice:", s) // [10 20 30 40 50]
// Reslice from index 1(inclusive) to 4(exclusive) (elements 20,30,40)
sub := s[1:4]
fmt.Println("Resliced slice:", sub) // [20 30 40]
// Modify resliced slice
sub[1] = 99
// Both slices share underlying array, so original changes too
fmt.Println("Modified resliced slice:", sub) // [20 99 40]
fmt.Println("Original slice after modification:", s) // [10 20 99 40 50]
}
- start is the starting index (inclusive)
- end is the ending index (exclusive)
- Both start and end must be within the sliceβs length (not capacity)
Trimming
Trimming is a special case of reslicing where you're dropping elements from the start or end of the slice.
s := []int{10, 20, 30, 40, 50}
// Trim first two elements
trimStart := s[2:] // [30 40 50]
// Trim last two elements
trimEnd := s[:3] // [10 20 30]
// Trim both sides
trimBoth := s[1:4] // [20 30 40]
Note:
- We are not deleting anything; we are just narrowing the view.
- The underlying array remains the same.
- This is very memory-efficient, but:
- The backing array may still keep the "trimmed" elements
- Can cause memory leaks if the backing array is large and not needed
7. About memory leaks in trimming
-
Go has garbage collection (GC) β it will automatically free memory that is no longer referenced. But GC can only collect memory that is truly unreachable.
-
When you reslice or trim a slice, the new slice still points to the original underlying array β even if you only use a small part of it.
-
The entire array stays in memory, because a portion of it is still referenced.
- If the original array is large, this causes memory to be retained unnecessarily.
- This isnβt a memory leak in the classical sense (like in C/C++), but itβs wasted memory; often called a βsemileakβ.
func leak() []int {
// Create a large array (e.g., 1 million elements)
large := make([]int, 1_000_000)
// Trim and return a small slice
return large[999_990:] // Just the last 10 elements
}
We might think that we are only keeping a slice of 10 elements, but:
- The slice still points to the full 1-million-element array.
- So Go cannot free that array, because 10 elements are still referenced.
- This can quietly accumulate over time β especially in long-running services.
8. make() and copy()
-
make() and copy() are two built-in functions in Go used primarily to work with slices, maps, and channels (not arrays directly).
-
make() is used to create and allocate memory for slices, maps, and channels.
-
copy() copies elements between slices.
-
Unlike range(), which is a language keyword used for iteration, make() and copy() are standard library functions.
src := []int{1, 2, 3}
dst := make([]int, len(src)) // create a new slice with the same length
copy(dst, src) // copy data from src to dst
dst[0] = 99 // modify dst
fmt.Println("src:", src) // [1 2 3] (unchanged)
fmt.Println("dst:", dst) // [99 2 3] (changed)
9. Shallow vs deep copy of slices
Deep copy:
- Copying the actual data into a new underlying array.
- The new slice is completely independent of the original.
- Changes to one will not affect the other.
Shallow copy:
- Only copies the slice header (pointer, length, capacity).
- Both original and shallowCopy point to the same underlying array.
- So, changing one affects the other.
package main
import "fmt"
func main() {
// Original slice
original := []int{1, 2, 3, 4, 5}
// Shallow copy: just assign the slice
shallowCopy := original
// Modify the "copy"
shallowCopy[0] = 99
// Print both slices
fmt.Println("Original slice:", original) // [99 2 3 4 5]
fmt.Println("Shallow copy: ", shallowCopy) // [99 2 3 4 5]
}
package main
import "fmt"
func main() {
// Original slice
original := []int{1, 2, 3, 4, 5}
// Create a new slice with same length
copySlice := make([]int, len(original))
// Copy data from original to copySlice
copy(copySlice, original)
// Modify the copy
copySlice[0] = 99
// Print both slices
fmt.Println("Original slice:", original) // [1 2 3 4 5]
fmt.Println("Copy slice: ", copySlice) // [99 2 3 4 5]
}
10. Multidimentional slices
rows, cols := 3, 4
matrix := make([][]int, rows) // Create outer slice with 3 rows
for i := range matrix {
matrix[i] = make([]int, cols) // Initialize each row with 4 columns
}
// Now matrix is 3x4 filled with zeros (default int value)
fmt.Println(matrix)
/* Output:
[[0 0 0 0]
[0 0 0 0]
[0 0 0 0]]
*/
Some examples for variety
package main
import "fmt"
func main() {
// Existing arrays (each represents a row)
row1 := [3]int{1, 2, 3}
row2 := [3]int{4, 5, 6}
row3 := [3]int{7, 8, 9}
// Convert arrays to slices and combine into a 2D slice
matrix := [][]int{
row1[:],
row2[:],
row3[:],
}
fmt.Println(matrix)
// Output:
// [[1 2 3]
// [4 5 6]
// [7 8 9]]
}
package main
import "fmt"
func main() {
// Define a 2D array (3x3)
var arr [3][3]int = [3][3]int{
{1, 2, 3},
{4, 5, 6},
{7, 8, 9},
}
// Create a 2D slice from the 2D array by slicing each row
matrix := make([][]int, len(arr))
for i := range arr {
matrix[i] = arr[i][:] // slice each row of the array
}
fmt.Println(matrix)
// Output:
// [[1 2 3]
// [4 5 6]
// [7 8 9]]
}
Slices are only 1D (2D copy not allowed)
Important:In Go, we cannot directly slice a multidimensional array as a whole into a multidimensional slice. Slicing works only on one-dimensional arrays or slices.
Given an array of:
var arr [3][3]int
Following is INVALID:
//Solution-1: Create a 2D slice from a 2D array (row-by-row slicing)
package main
import "fmt"
func main() {
arr := [3][3]int{
{1, 2, 3},
{4, 5, 6},
{7, 8, 9},
}
// Create 2D slice from 2D array
matrix := make([][]int, len(arr))
for i := range arr {
matrix[i] = arr[i][:] // slice each row (array -> slice)
}
fmt.Println(matrix)
// Output:
// [[1 2 3]
// [4 5 6]
// [7 8 9]]
}
11. Slices to pass-by-reference
package main
import "fmt"
// Function modifies the slice
func modifySlice(s []int) {
s[0] = 100 // this will affect the original array
}
func main() {
// Original array
arr := [3]int{1, 2, 3}
// Create a slice from the array
slice := arr[:] // full slice of array
fmt.Println("Before:")
fmt.Println("Array:", arr)
fmt.Println("Slice:", slice)
// Pass slice to function (modifies underlying array)
modifySlice(slice)
fmt.Println("\nAfter:")
fmt.Println("Array:", arr) // changed!
fmt.Println("Slice:", slice) // also changed
}
12. When to prefer slices (over arrays)
A simple rule of thumb:
- Use slices almost always.
- Use arrays only when you have a specific reason β like fixed size, performance constraints, or interfacing with C code.
- When pass-by-reference is important due to performance considrations.
//prefer slices over arrays (like this)
names := []string{"Alice", "Bob"}
names = append(names, "Carol") // dynamic growth
Use arrays when:
- We are writing low-level or embedded, because they have less GC overhead, and tighter control.
- We are interfacing with C code via cgo, because C functions often expect arrays.