When I started learning Go, arrays and slices seemed almost identical. Both store a sequence of values, both let us access elements using indexes, and both look similar when we write code.
But as I explored them, I realized that the way Go handles arrays and slices is quite different.
The most important differences come down to memory, assignment, and how we pass them to functions.

In this post, I’ll walk through arrays and slices step by step, using small examples to understand what happens behind the scenes.
1. What Are Arrays and Slices?
Go has several built-in container types. Three important ones are:
- Arrays
- Slices
- Maps
Arrays and slices store sequences of elements, much like strings store sequences of bytes. A map is different because it associates keys with values.
Let’s start with arrays.
2. Arrays in Go: A Fixed Number of Elements
An array has a fixed size. When we create an array, we specify how many elements it contains.
package main
import "fmt"
func main() {
var a [3]int
b := [3]int{10, 20, 30}
c := [...]int{10, 20, 30}
fmt.Println(a)
fmt.Println(b)
fmt.Println(c)
}
Output:
[0 0 0]
[10 20 30]
[10 20 30]
Let’s understand the declarations:
- var a [3]int creates an array of three integers. Each element starts with its zero value, which is 0.
- b := [3]int{10, 20, 30} creates an array and initializes its elements.
- c := [...]int{10, 20, 30} lets Go determine the array size from the number of values in the initializer.
All three variables are arrays of three integers.
Once an array is created, its size is fixed. We cannot simply add another element to make it an array of four integers.
Array size is part of its type
Consider these declarations:
var a [3]int
var b [4]int
Although both arrays contain integers, their types are different.
An array of three integers is not the same type as an array of four integers. Therefore, we cannot assign one directly to the other.
a = b // Compile-time error
This is one reason arrays are less flexible when we need a collection whose size changes.
3. Arrays Are Copied When Assigned
This is one of the most important things to understand about arrays in Go.
An array is a sequence of elements stored together. When we assign one array to another, Go copies the elements into the new array.
Let’s see what that means.
package main
import "fmt"
func main() {
a := [3]int{10, 20, 30}
b := a
b[0] = 100
fmt.Println("a:", a)
fmt.Println("b:", b)
}
Output:
a: [10 20 30]
b: [100 20 30]
When we wrote:
b := a
Go copied the elements of a into b.
The two arrays are independent. Changing an element in b does not change the corresponding element in a.
This is different from the way slices behave, as we’ll see shortly.
Copying arrays can be convenient when the array is small. However, copying large arrays can become inconvenient because all of their elements are copied.
4. What Is a Slice?
A slice is similar to an array, but it is much more flexible.
A slice has a variable length, and it refers to an underlying array that holds the actual elements.
When we create a slice, Go can provide the underlying array for us.
Here is a simple example:
package main
import "fmt"
func main() {
s := []int{10, 20, 30}
fmt.Println(s)
}
Output:
[10 20 30]
Notice the difference in the declarations:
[3]int // Array of three integers
[]int // Slice of integers
The empty brackets indicate a slice.
Unlike an array, the slice itself does not contain all the elements directly. Instead, it acts as a descriptor that refers to the underlying array.
Conceptually, a slice descriptor contains three things:
- A pointer to the underlying array.
- The length of the slice.
- The capacity of the slice.
We can think of it like this:
Conceptual view of a slice
Slice descriptor
+----------------+
| Pointer | --------+
| Length | |
| Capacity | |
+----------------+ |
v
Underlying array
+------+------+------+------+
| 10 | 20 | 30 | ... |
+------+------+------+------+
The key idea is that the slice refers to the array. The descriptor tells Go where the elements are and how much of the underlying array the slice can use.
This makes slices flexible: we can create new slice descriptors that refer to different portions of the same underlying array.
5. Slices Can Be Modified
Unlike strings, whose contents are immutable, slices allow us to modify their elements.
package main
import "fmt"
func main() {
s := []int{10, 20, 30}
s[0] = 100
fmt.Println(s)
}
Output:
[100 20 30]
We changed the first element directly.
This is possible because a slice provides access to elements in its underlying array.
We can also change the length of a slice by creating a new slice or using the built-in append function, which we'll discuss below.
6. Slice Assignment: Two Slices Can Share the Same Array
Here is where slices become particularly interesting.
When we assign one slice to another, Go copies the slice descriptor, not all the elements of the underlying array.
Let’s look at an example.
package main
import "fmt"
func main() {
a := []int{10, 20, 30}
b := a
b[0] = 100
fmt.Println("a:", a)
fmt.Println("b:", b)
}
Output:
a: [100 20 30]
b: [100 20 30]
Why did changing b also change a?
Because both slices refer to the same underlying array.
When we write:
b := a
Go copies the descriptor. Both descriptors now refer to the same elements in memory.
Conceptually:
a ----+
|
v
[10, 20, 30]
^
|
b ----+
When we change an element through either slice, we modify the shared array.
This behavior is important to remember when passing slices around in a program. A change made through one slice may be visible through another slice that shares its underlying array.
7. Understanding Length and Capacity
Every slice has a length and a capacity.
The length tells us how many elements are currently accessible through the slice.
The capacity tells us how many elements the slice can contain from its starting position before it needs a larger underlying array.
We can inspect these values using the built-in functions len and cap.
package main
import "fmt"
func main() {
s := []int{10, 20, 30}
fmt.Println("Length:", len(s))
fmt.Println("Capacity:", cap(s))
}
Output:
Length: 3
Capacity: 3
For this slice, the length and capacity are both three.
The important distinction is that length and capacity are not always equal.
For example, a slice can refer to only part of an underlying array. Its length may be smaller than the number of elements available from its starting position.
8. Using append to Add Elements
One of the most common operations with slices is adding elements.
Go provides the built-in append function for this purpose.
package main
import "fmt"
func main() {
s := []int{10, 20, 30}
s = append(s, 40)
fmt.Println(s)
}
Output:
[10 20 30 40]
Notice that we assign the result of append back to s:
s = append(s, 40)
The append function returns a slice containing the additional element.
If the existing underlying array has enough capacity, the new element can be added to that array. If there isn’t enough capacity, Go can allocate a new underlying array and copy the existing elements into it.
That’s why it is important to use the slice returned by append.
We can also append multiple elements:
s = append(s, 50, 60)
Now s contains:
[10 20 30 40 50 60]
This is one of the reasons slices are so useful: they can grow as we add elements.
9. Slicing: Selecting a Portion of a Slice
We can create a new slice that refers to a portion of an existing slice using slicing expressions.
The general form is:
s[low:high]
The lower index is included, but the upper index is excluded.
Let’s see this in action.
Go
package main
import "fmt"
func main() {
s := []int{100, 105, 110, 115, 120, 125}
fmt.Println(s[1:4])
}
Output:
[105 110 115]
The expression s[1:4] includes indexes 1, 2, and 3.
It does not include index 4.
Remember that Go indexes begin at zero.
| Index | Value |
|:------:|------:|
| `0` | `100` |
| `1` | `105` |
| `2` | `110` |
| `3` | `115` |
| `4` | `120` |
| `5` | `125` |
So the resulting slice contains three elements.
Its length is:
4 - 1 = 3
Omitting the lower or upper bound
We can leave out one of the bounds.
s := []int{100, 105, 110, 115, 120, 125}
fmt.Println(s[:3])
fmt.Println(s[2:])
Output:
[100 105 110]
[110 115 120 125]
The first expression starts at the beginning and stops before index 3.
The second starts at index 2 and continues to the end of the slice.
We can also use a slicing expression like this:
fmt.Println(s[3:5])
Output:
[115 120]
The result has length two because the upper bound is excluded.
10. Passing Arrays and Slices to Functions
Arrays and slices behave differently when we pass them to functions.
Let’s compare them.
package main
import "fmt"
func changeArray(a [3]int) {
a[0] = 100
}
func changeSlice(s []int) {
s[0] = 100
}
func main() {
w := [3]int{1, 2, 3}
x := []int{1, 2, 3}
changeArray(w)
changeSlice(x)
fmt.Println("w:", w)
fmt.Println("x:", x)
}
Output:
w: [1 2 3]
x: [100 2 3]
Let’s break down what happened.
When we called:
changeArray(w)
Go copied the array into the function parameter. The function modified its own copy, so the original array remained unchanged.
But when we called:
changeSlice(x)
Go copied the slice descriptor into the function parameter. The copied descriptor still referred to the same underlying array.
Therefore, modifying an element through the parameter changed the original slice’s underlying data.
This is a fundamental difference between arrays and slices in Go.
11. Using copy to Copy Slice Elements
Sometimes we don’t want two slices to share the same underlying array. We want to copy elements from one slice into another.
Go provides the built-in copy function.
package main
import "fmt"
func main() {
a := []int{1, 2, 3}
b := make([]int, 3)
n := copy(b, a)
fmt.Println("Copied:", n)
fmt.Println("a:", a)
fmt.Println("b:", b)
}
Output:
Copied: 3
a: [1 2 3]
b: [1 2 3]
The copy function copies elements from the source slice into the destination slice.
It copies as many elements as will fit in both slices. In other words, the number of copied elements is the smaller of the two lengths.
For example:
a := []int{1, 2, 3}
b := make([]int, 2)
n := copy(b, a)
fmt.Println(n)
fmt.Println(b)
Output:
2
[1 2]
Only two elements were copied because the destination slice has length two.
This is useful when we need a separate copy of some slice data rather than simply assigning one slice to another.
12. When Should We Use Arrays Instead of Slices?
In everyday Go programming, slices are used much more often than arrays.
Slices have variable length, are convenient for function parameters, and work naturally with operations such as append.
Arrays still have important uses.

One example is a fixed-size data structure used in an encryption algorithm. If a sequence must always contain exactly 64 bytes, an array can represent that fixed size directly.
Arrays can also be useful when we want value-copying behavior.
Slices, on the other hand, are usually the natural choice when we need a collection that can grow or when we want to work with arbitrary sequences of elements.
13. Nil Slices and Useful Zero Values
Go initializes variables to their zero values when we declare them without an explicit initial value.
For a slice, the zero value is nil.
package main
import "fmt"
func main() {
var s []int
fmt.Println(s)
fmt.Println(len(s))
fmt.Println(cap(s))
}
Output:
[]
0
0
A nil slice has no underlying array associated with it, but it can still be useful.
For example, we can append elements to a nil slice:
var s []int
s = append(s, 10)
s = append(s, 20)
fmt.Println(s)
Output:
[10 20]
This is one of the useful properties of Go’s zero values: we don’t always need to initialize a slice with make before we can use it.
Key Takeaways
- Arrays have a fixed size, and assigning an array copies its elements.
- Slices are descriptors that refer to an underlying array.
- Assigning a slice copies the descriptor, so slices can share underlying data.
- len tells us the number of accessible elements, while cap tells us the available capacity.
- append returns a slice that includes the appended elements.
- Slicing uses a half-open range: the lower bound is included and the upper bound is excluded.
- The copy function copies elements between slices.
- Slices are generally more common in Go because of their flexibility.
Conclusion
Arrays and slices may look similar, but understanding how they work in memory makes a big difference.
Arrays are fixed-size values that get copied when assigned. Slices are flexible descriptors that let us work with an underlying array without copying all its elements every time we assign a slice.
Once I understood the difference between copying an array and copying a slice descriptor, many of Go’s behaviors started to make sense.
If you’re learning Go, try experimenting with array , slice , append, and copy. Seeing how the values change will help make these concepts much easier to remember.
What surprised you most about arrays and slices in Go?
#Go #Golang #Programming #LearnGo #SoftwareDevelopment #Coding