Hello, I'm Ganesh. I'm building git-lrc, an AI code reviewer that runs on every commit. It is free, unlimited, and source-available on Github. Star Us to help devs discover the project. Do give it a try and share your feedback for improving the product.
Slices are one of the most commonly used data structures in Go.
They appear simple on the surface, but their design is carefully engineered to provide flexibility without sacrificing performance.
In this article, we will examine how Go slices work internally and analyze the algorithm that enables them to grow dynamically while remaining efficient.
What are Slices?
Slices are dynamic arrays. It uses pointers to refer to the underlying array. Further below, we will understand more in detail.
How to create slices?
Initialize the array and create a slice from it.
For creating a slice from an array, we use [start:end] notation.
start is the starting index of the slice.
end is the ending index of the slice.
import "fmt"
func main() {
s := [5]int{10, 20, 30, 40,50}
fmt.Println(s)
var s1 []int = s[1:3]
fmt.Println(s1)
}
Output:
Using Structs with Slices
We can store any data type in a slice.
For example, we can store a struct in a slice.
package main
import "fmt"
type Person struct {
Name string
Age int
}
func main() {
people := []Person{
{Name: "John", Age: 30},
{Name: "Jane", Age: 25},
}
fmt.Println(people)
}
The output of the code will have JSON-like output inside square brackets.
Output:
So, we understood how to create slices. But to understand how Go creates and how it handles slices, we have to understand the algorithm behind it. Let's see the source code of Go. link
How Slices Work Internally?
Slices will have 3 components.
- Pointer to the underlying array.
- Length of the slice.
- Capacity of the slice.
It is a little bit confusing, let's understand with the example below.

Now by above image, we can understand how slices are created internally.
Modifying Slices
When we do slicing from a new slice with a different start point, it will create a new slice with a different pointer.
If we change the pointer, then the data from the previous pointer will be lost.
To identify how it works, let's see the example below.
package main
import "fmt"
func main() {
s := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
printSlice(s)
s = s[1:5]
printSlice(s)
s = s[2:]
printSlice(s)
s = s[:1]
printSlice(s)
}
func printSlice(s []int) {
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}
Here is the visual representation.

Output:
This is actual output of the code.
gk@jarvis:~/exp/code/rd/go-exmaple$ go run main.go
len=10 cap=10 [1 2 3 4 5 6 7 8 9 10]
len=4 cap=9 [2 3 4 5]
len=2 cap=7 [4 5]
len=1 cap=7 [4]
I explained how empty data is represented in slices.
How Empty Slice is Represented?
package main
import "fmt"
func main() {
var s []int
fmt.Println(s, len(s), cap(s))
if s == nil {
fmt.Println("s is nil")
}
}
Output:
len and cap are 0 because the slice is not initialized.
When we initialize a slice, it will have a defined length and capacity.
The underlying pointer will point to the first element of the backing array, and both the length and capacity will be set to the number of elements in that array.
Initializing a Slice
We can initialise slices with different data types.
Initializing a Slice with make
make is a predefined function in Go used to create and initialize slices, maps, and channels.
Reference variables like slices are typically initialized using the make command from Go's built-in functions.
package main
import "fmt"
func main() {
a := make([]int, 5)
fmt.Println(a, len(a), cap(a))
b := make([]int, 5, 10)
fmt.Println(b, len(b), cap(b))
}
Output:
[0 0 0 0 0] 5 5
[0 0 0 0 0] 5 10
This allows you to create a slice with a specific length and capacity.
make([]T, len, cap)
- For
a := make([]int, 5), it creates a slice of length 5 and capacity 5.
- For
b := make([]int, 5, 10), it creates a slice of length 5 and capacity 10.
Variable Declaration
You can also initialize a slice directly using a slice literal:
package main
import "fmt"
func main() {
a := []int{1, 2, 3, 4, 5}
fmt.Println(a, len(a), cap(a))
}
Output:
For this slice, the pointer points to the first element of the underlying array, and the length and capacity are set to the number of elements provided.
Now, we will see how to initialize 2D slices and how to append to a slice.
Initializing 2D Slices
package main
import (
"fmt"
"strings"
)
func main() {
ticTacToeBoard := [][]string{
{"_", "_", "_"},
{"_", "_", "_"},
{"_", "_", "_"},
}
ticTacToeBoard[0][0] = "X"
ticTacToeBoard[2][2] = "O"
fmt.Println(ticTacToeBoard)
for i := 0; i < len(ticTacToeBoard); i++ {
fmt.Println(strings.Join(ticTacToeBoard[i], " "))
}
}
We are initializing a 2D slice of strings with 3 rows and 3 columns.
We can modify the values of the slice using the index variable[x][y].
Finally, we are printing the slice using a loop.
Each