Welcome,
In this article we're going to discuss all there is to know about arrays and slices in Go, we'll go deep talking about their internal structures and why they behave very differently even though they do similar things. We'll be going through Arrays and Slices and their behaviors and difference under 5 topics;
- Default or "zero" values
- Declaration and Initializing Arrays and Slices
- Value parts of Arrays and Slices
- Manipulating Arrays and Slices
- Potential quirks of slices
- Tips for optimizing performance in your code
Things to note
-
len
is a built-in function that returns the number of items in an array of slice -
cap
is a built-in function that returns the capacity of an array or slice. The capacity of an array is equal to it's length and the capacity of a slice is the maximum number of elements that the slice can hold before it needs to be resized (slice capacity may be greater than the length) -
fmt.Println
is a function in thefmt
package in Go that is used to print a line to the standard output. It takes one or more values as arguments and prints them to the console, separated by spaces and followed by a newline.
Default or "zero" values
In Go, if you declare a variable without explicitly initializing it, the variable is automatically set to the zero value for its type. The zero value is a default value assigned to a variable of a specific type when it is declared but not explicitly initialized. For example, if you declare an int variable like this:
var x int
The value of x will be initialized to 0.
In other languages like Javascript value of an uninitialized variable is undefined
The zero value for each type is as follows:
-
int
: 0 -
float
: 0.0 -
bool
: false -
string
: "" (empty string) -
pointer
: nil -
struct
: all fields are set to the zero value for their respective types
The zero value of an array in Go is an array with all elements set to the zero value for their respective types. For example, if you have an array of integers:
var arr [5]int
The zero value of this array would be:
[0, 0, 0, 0, 0]
Similarly, if you have an array of strings:
var arr [5]string
The zero value of this array would be:
["", "", "", "", ""]
In Go, the zero value of a slice is nil, which is a slice with a length of 0, a capacity of 0, and no underlying array. For example:
var slice []int
fmt.Println(slice == nil) // => true
Declaring and Initializing Arrays
The format for declaring arrays in Go is var name [L]T
.
var
is a keyword for declaring all kinds of variables in Go
name
is the name of the variable, can be anything
L
is the length of the array (must be a constant) and T
is the type of the array items.
Let's look at some examples
//Array of 5 Intergers
var nums [5]int
fmt.Println(nums) // => [0 0 0 0 0]
//Array of 10 strings
var strs [10]string
fmt.Println(nums) // => [ ]
// Nested arrays
var nested = [3][5]int{
{1, 2, 3, 4, 5},
{6, 7, 8, 9, 10},
{11, 12, 13, 13, 15},
}
fmt.Println(nested) // => [[1 2 3 4 5] [6 7 8 9 10] [11 12 13 13 15]]
Initializing arrays is simply assigning a value to the variable
var name = [L]T{...}
where ...
represents the array items of type T
//Intializing an array containing 10 intergers
var nums = [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
fmt.Println(nums) // => [1 2 3 4 5 6 7 8 9 10]
//Intializing an array containing 10 strings
var strs = [10]string{"one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten"}
fmt.Println(strs) // => [one two three four five six seven eight nine ten]
//Nested arrays
var nested = [3][2]int{}
You can also create an array of structs
type Car struct {
Brand string
Color string
Price float32
}
//Array of 5 items of type Car
var arrayOfCars = [5]Car{
{Brand: "Porsche", Color: "Black", Price: 20_000.00},
{Brand: "Volvo", Color: "White", Price: 8_000.00},
{Brand: "Honda", Color: "Blue", Price: 7_000.00},
{Brand: "Tesla", Color: "Black", Price: 50_000.00},
{Brand: "Kia", Color: "Red", Price: 5_000.98},
}
fmt.Println(arrayOfCars) // => [{Porsche Black 20000} {Volvo White 8000} {Honda Blue 7000} {Tesla Black 50000} {Kia Red 5000.98}]
To create an array of items with different types in Go, you can use the interface{}
type.
An interface in Go is a type that defines a set of methods that a type must implement. Any type that implements all of the methods listed in an interface is said to satisfy the interface and is considered to be of that interface type. The special interface type interface{}
has no methods, which means that every type in Go satisfies this interface.
package main
import "fmt"
func main() {
//Array containing 5 items of different type
var randomsArray = [5]interface{}{"Hello world!", 35, false, 33.33, 'A'}
fmt.Println(randomsArray) // => [Hello world! 35 false 33.33 65]
}
Other ways of initializing arrays
import "fmt"
func main() {
// Using shorthand syntax
cars := [3]string{"Tesla", "Ferrari", "Benz"}
fmt.Println(cars) // => [Tesla Ferrari Benz]
// Using ... inplace of array length
digits := [...]int{10, 20, 30, 40}
fmt.Println(digits) // => [10 20 30 40]
// Using len keyword
countries := [len(digits)]string{"China", "India", "Kenya"}
fmt.Println(countries) // => [China India Kenya]
}
Note you can't use the :=
shorthand syntax in global scope
Declaring and Initializing Slices
To declare a slice we use the format var name []int
, the only difference between declaring arrays and slices is that with slices we omit the length.
Examples
import "fmt"
func main() {
// A slice of intergers
var intSlice []int
fmt.Println(intSlice) // => []
// A slice of intergers
var stringSlice []string
fmt.Println(stringSlice) // => []
}
To initialize a slice in Go, you can use the make function. The make function takes three arguments: the type of the slice, the length of the slice, and the capacity of the slice (which is optional). make([]T, len, cap)
For example, to create a slice of integers with a length of 5 and a capacity of 10, you can use the following code:
package main
import "fmt"
func main() {
// With capacity
slice1 := make([]int, 5, 10)
fmt.Println(len(slice1), cap(slice1)) // => 5 10
// Without capacity
slice2 := make([]int, 5)
fmt.Println(len(slice2), cap(slice2)) // => 5 5
}
When you omit the capacity, the capacity is set to the length of the slice.
You also initialize a slice without the make
function by immediately assigning value to it
slice := []int{1, 2, 3}
fmt.Println(len(slice), cap(slice)) // => 3 3
Value parts of Arrays and Slices
This is the most important difference between arrays and slices in Go, Arrays have only one part whiles slices have direct and indirect parts (2 parts). What this means is that arrays in Go are fixed-length data structures that consist of a contiguous block of memory for storing elements. Slices are dynamically-sized and reference a contiguous segment of an underlying array.
To understand this properly, let's look at the value parts of the this array and slice example
var arr = [5]int{1,2,3,4,5}
var slice = []int{1,2,3,4,5}
From the above diagrams we can see that an array is a fixed-size collection of elements of the same type, stored in contiguous memory locations. On the other hand, a slice consists of a pointer to an underlying array, a length, and a capacity.
internal structure of a slice direct part
type _slice struct {
// referencing underlying elements
elements unsafe.Pointer
// number of elements
len int
// capacity of the slice
cap int
}
What happens when you copy Arrays and Slices
In Go, underlying value parts are not copied in value assignments only direct values a copied. What this means is that when we copy an array we're making a copy of it's elements (because it's has only a direct part) but when we copy a slice, we are making a copy of it's direct part i.e len
, cap
and pointer to elements
and the indirect part (the actual elements are not copied)
Array copy example
x := [5]int{3, 6, 9, 12, 15}
y := v
In the above code example we've initialized an array x
and then we created another variable y
and we assigned (copied) the value of x
to y
.
Here is a diagram representation of what happens
when we make a copy of an array all element are copied into a separate memory block. In the above code example, if we make changes to to x
it doesn't affect y
and the vice versa, we'll talk more on that later
Slice copy example
x := []int{2,4,6,8,10}
y := x
In the above code example we've initialized a slice x
and then we created another slice y
and we assigned (copied) the value of x
to y
.
From the above diagram you can see that the direct parts of x
is copied into a separate memory for y
(Len, Cap, and Elem pointer) but x
and y
still share the same underlying parts (array elements). So when you modify an element of x
it also affects y
since the share the same underlying elements, but x
and y
can have different lengths and capacity since those are in their own separate memories
more on this later
Manipulating Arrays and Slices
In this section, we will discuss manipulating arrays and slices.
Arrays
Since arrays in Go have a fixed constant length, the only manipulation that can be done on arrays is changing values at specific indexes starting from zero.
first element is 0
second element is 1
third element is 2
and so on.
Examples
package main
import "fmt"
func main() {
var fruits [6]string // Declare string array with zero values
fmt.Println(fruits) // => [ ] (string zero value "")
// 🍊 change first element
fruits[0] = "Orange"
fmt.Println(fruits) // => [Orange ]
//🍋 change last element
fruits[5] = "Lemon"
fmt.Println(fruits) // => [Orange Lemon]
// 🍌🍉🍐🍏change all
fruits[1] = "Banana"
fruits[2] = "Watermelon"
fruits[3] = "Pear"
fruits[4] = "Apple"
fmt.Println(fruits) // => [Orange Banana Watermelon Pear Apple Lemon]
//🍍 Dont' like oranges? change first element again
fruits[0] = "Pineapple"
fmt.Println(fruits) // => [Pineapple Banana Watermelon Pear Apple Lemon]
//Modify array of integers
evenNumbers := [5]int{2, 4, 6, 8, 10}
evenNumbers[0] = 12
fmt.Println(evenNumbers) // => [12 4 6 8 10]
evenNumbers[3] = 20
fmt.Println(evenNumbers) // => [12 4 6 20 10]
}
Accessing array values
import "fmt"
func main() {
nums := [7]int{1, 2, 3, 4, 5, 6, 7}
// get the first element
first := nums[0]
fmt.Println(first) // => 1
// get third element
fmt.Println(nums[2]) // => 3
// last
fmt.Println(nums[6]) // => 7
//alternatively
fmt.Println(nums[len(nums)-1]) // => 7
}
If we try to change element an index greater than or equal to the arrays length the code will won't compile it'll panic
with an index out of bound error
package main
import "fmt"
func main() {
nums := [7]int{1, 2, 3, 4, 5, 6, 7}
outOfBound := nums[7]
}
invalid argument: array index 7 out of bounds [0:6]
Slices
Slices are a useful data type in Go because they offer a flexible and convenient way to manipulate collections of data. They can be accessed and modified in the same way as arrays, but they also have some specific behaviors that make them more powerful. In the following sections, we will explore some of these behaviors in more detail.
Slice expression
The slice expression signature is s[start:end:cap]
which is used to create a new slice that includes all the elements of the original slice s
, starting from the element at index start
and up to but not including the element at index end
, the cap
is the capacity of the newly created sublice and it's optional. If cap
is omitted, the capacity of the sub slice is equal to it's length. The length of the sub slice is end
- start
Example
package main
import "fmt"
func main() {
slice := []int{1, 2, 3, 4, 5, 6}
subSlice := slice[1:4]
fmt.Println(subSlice) // => [2 3 4]
fmt.Println(len(subSlice), cap(subSlice)) // => 3 3
subSliceWithCap := slice[1:4:5]
fmt.Println(subSliceWithCap) // => [2 3 4]
fmt.Println(len(subSliceWithCap), cap(subSliceWithCap)) // => 3 4
}
note the capacity also starts from zero, so if you use 5
as capacity of a slice s
calling cap(s)
will return 4
If the start
index is zero, you can omit it s[:end]
similarly if the end
index is the end of the array you can omit it like so s[start:]
Examples
package main
import "fmt"
func main() {
s := []string{"g", "o", " ", "i", "s", " ", "s", "w", "e", "e", "t"}
// copy from 0 to index 2 (index 2 is exclusive)
goSubSlice := s[:2]
fmt.Println(goSubSlice) // => [g o]
// copy from index 3 to end
isSweetSubSlice := s[3:]
fmt.Println(isSweetSubSlice) // => [i s s w e e t]
// copy all items
copySlice := s[:]
fmt.Println(copySlice) // => [g o i s s w e e t]
}
Earlier we discussed slice value parts and how only the direct parts of a slice is copied, so when we create a sub slice what is actually happening ?
Let's use this examples
package main
import "fmt"
func main() {
n := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
n1 := n[:6]
n2 := n[3:8]
n3 := n[4:10]
fmt.Println(n1, len(n1), cap(n1)) // => [1 2 3 4 5 6] 6 10
fmt.Println(n2, len(n2), cap(n2)) // => [4 5 6 7 8] 5 7
fmt.Println(n3, len(n3), cap(n3)) // => [5 6 7 8 9 10] 6 6
// change n1 at index 4 to 15
n1[4] = 15
fmt.Println(n) // => [1 2 3 4 15 6 7 8 9 10]
fmt.Println(n1) // => [1 2 3 4 15 6]
fmt.Println(n2) // => [4 15 6 7 8]
fmt.Println(n3) // => [15 6 7 8 9 10]
}
Notice how when we changed the n1[4]
to 15 it affected all the other sub slices including the main slice? that's because they share the same underlying array elements, so anytime we make changes to a sub slice it's affects all other sub slices.
Here is a diagram of the above slice and sub slices to help you understand what's happening
From the above diagram we can see all sub slices share the same underlying array but they span different parts of the underlying array. When we make a change at a particular index of a sub slice, all sub slices the span that index are are also modified.
When you modify the element at index 4 of n3
, you are also modifying the element at index 8 of the array (n
), because n3
and n share the same underlying array. As a result, the change to n3[4]
is also reflected in n
. Similarly, any changes made to the elements of n
between indices 4 and 9 (inclusive) will also be reflected in n3
, because n3
spans those indices.
n3[4] = 18
fmt.Println(n) // => [1 2 3 4 15 6 7 8 18 10]
fmt.Println(n1) // => [1 2 3 4 15 6]
fmt.Println(n2) // => [4 15 6 7 8]
fmt.Println(n3) // => [15 6 7 8 18 10]
Appending items to a slice
The Go append function allows you to add elements to the end of a slice. It has the following syntax:
func append(s []T, x ...T) []T
s is the slice you want to append to, x
is a list of one or more elements of type T
to be appended, and the function returns a new slice with the appended elements.
For example:
s := []int{1, 2, 3}
s = append(s, 4, 5, 6)
fmt.Println(s) // => s is now [1, 2, 3, 4, 5, 6]
Note that if the capacity of the underlying array is insufficient to accommodate the additional elements, append will allocate a new, larger array to hold the result. If a new larger array is created after append, that slice does no longer share the same underlying array with other sub slices
Let's use the previous example
package main
import "fmt"
func main() {
n := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
n1 := n[:6]
n2 := n[3:8]
n3 := n[4:10]
fmt.Println(n1, len(n1), cap(n1)) // => [1 2 3 4 5 6] 6 10
fmt.Println(n2, len(n2), cap(n2)) // => [4 5 6 7 8] 5 7
fmt.Println(n3, len(n3), cap(n3)) // => [5 6 7 8 9 10] 6 6
}
from the above code n2
has a length of 5 and a capacity of 7 which means we can append two more items without a new array being created and it'll continue so share the same underlying array with the other sub slices
See
n2 = append(n2, 100)
fmt.Println(n) // => [1 2 3 4 15 6 7 8 100 10]
fmt.Println(n1) // => [1 2 3 4 15 6]
fmt.Println(n2) // => [4 15 6 7 8, 100]
fmt.Println(n3) // => [15 6 7 8 100 10]
n2 = append(n2, 101)
fmt.Println(n) // => [1 2 3 4 15 6 7 8 100 101]
fmt.Println(n1) // => [1 2 3 4 15 6]
fmt.Println(n2) // => [4 15 6 7 8 100 101]
fmt.Println(n3) // => [15 6 7 8 100 101]
// Check the capacity and length of n2
fmt.Println(cap(n2), len(n2)) // => 7 7
As we appended more items it was affecting n
and n3
but now the n2
slice is full (capacity == length).
Now that capacity of n2 is equal to it's length so appending a new item will cause new array to created for n2 and it will no longer share the same underlying array with the other sub slices
n2 = append(n2, 102)
fmt.Println(n) // => [1 2 3 4 15 6 7 8 100 101]
fmt.Println(n1) // => [1 2 3 4 15 6]
fmt.Println(n2) // => [4 15 6 7 8 100 101 102]
fmt.Println(n3) // => [15 6 7 8 100 101]
From the above code you can see only n2
was modified and the others weren't affected.
appending multiple items to a slice
example
package main
import "fmt"
func main() {
s := []int{10, 20, 30, 40, 50, 60}
s2 := []int{70, 80, 90}
// Appending slice to slice
s = append(s, s2...)
fmt.Println(s) // => [10 20 30 40 50 60 70 80 90]
// Appending multiple values
s = append(s, 100, 110, 120)
fmt.Println(s) // => [10 20 30 40 50 60 70 80 90 100 110 120]
}
Deep value copying of a slice
Deep copying simply means copying the underlying array of a slice instead of the direct part so the destination slice doesn't share the same underlying memory with the source slice.
deep copying using append
package main
import (
"fmt"
)
func main() {
slice1 := []int{1, 2, 3, 4, 5, 6}
slice2 := []int{}
slice2 = append(slice2, slice1...)
fmt.Println(slice1) // => [1 2 3 4 5 6]
fmt.Println(slice2) // => [1 2 3 4 5 6]
//Modifying slice2 doesn't affect slice1
slice2[0] = 100
fmt.Println(slice1) // => [1 2 3 4 5 6]
fmt.Println(slice2) // => [100 2 3 4 5 6]
// copying a range of items
slice3 := []int{}
slice3 = append(slice3, slice1[3:5]...)
fmt.Println(slice3) // => [4 5]
//Again slice3 and slice1 doesn't share underlying array
slice3[0] = -10
fmt.Println(slice1) // => [1 2 3 4 5 6]
fmt.Println(slice3) // => [-10 5]
}
In Go, you can use the copy function to perform a deep copy of a slice. A deep copy creates a new slice with its own, independent copy of the elements of the original slice.
The copy function has the following syntax:
func copy(dst, src []T) int
dst
is the destination slice, and src
is the source slice. Both slices must have the same element type T
. The function returns the number of elements copied, which will be the minimum of the lengths of dst
and src
.
For example:
package main
import "fmt"
func main() {
s := []int{1, 2, 3}
t := make([]int, len(s))
copy(t, s)
fmt.Println(t) // => [1, 2, 3], and is a deep copy of s
// copy a range of items
t = make([]int, len(s)-1)
copy(t, s[0:2])
fmt.Println(t) // => [1, 2], and is a deep copy of s
}
Note that if the length of dst
is less than the length of src
, only the first len(dst)
elements of src
will be copied. To make a deep copy of the entire slice, you must ensure that dst
has sufficient capacity to hold all of the elements of src
.
Potential quirks of slices
As mentioned earlier, a sub slice only copies the direct part of a slice and share the same underlying array. So when we have a slice sBig
of size 10 mega bytes and we create a sub slice sTiny
of size 3 bytes from sBig
, sTiny
and sBig
will reference the same underlying array. As you might know Go is garbage collected which means it automatically frees up memory when it is not referenced (it can not be reached).
So in the case of sBig
and sTiny
even if we only need sTiny
which is 3 bytes, sBig
will continue to be in memory because sTiny
references the same underlying array as sBig
. To get around this we make a deep copy so that sTiny
doesn't share the underlying array as sBig
and so it can be garbage collected thereby freeing up memory.
Example
var gopherRegexp = regexp.MustCompile("gopher")
func FindGopher(filename string) []byte {
//Reading a very huge file 1,000,000,000 bytes (1GB)
b, _ := ioutil.ReadFile(filename)
//Taking a just 6 byte sub slice
gopherSlice := gopherRegexp.Find(b)
return gopherSlice
}
From the above example we read a very huge file (1GB) and returned a sub slice of it (just 6 bytes), since the gopherSlice
still reference the same underlying array as the huge file, which means that 1GB of memory can not be garbage collected even though we are not using it anymore.
If you call the FindGopher
function multiples times, you program can eat all the computers memory. To fix this, like I said earlier we make a deep copy so gopherSlice
doesn't share the same underlying array as the huge slice
Example
var gopherRegexp = regexp.MustCompile("gopher")
func FindGopher(filename string) []byte {
//Reading a very huge file 1,000,000,000 bytes (1GB)
b, _ := ioutil.ReadFile(filename)
//Taking a just 6 byte sub slice
gopherSlice := make([]byte, len("gopher"))
// Make a deep copy
copy(gopherSlice, gopherRegexp.Find(b...)
return gopherSlice
}
Now the Go language garbage collector can now free up the ~1GB of memory
Tips for optimizing performance in your code
Like I said earlier the most important difference between arrays and slices in Go is the difference in their value parts this together with Go value copy cost is the reason they differ in performance.
Value copy cost
Value assignments, argument passing, looping using range
keyword etc. all involve value copying. The bigger the value size the bigger the value copy cost, copying 10 mega bytes will take longer than copying 10 bytes. And we also learned that only direct parts are copied.
Example
array := [100]int{1,2,3,4,5,6, ..., 100}
slice := []int{1,2,3,4,5,6, ..., 100}
From the above example we've created an array of numbers 1-100
and a slice also of numbers 1-100
. When we copy the array all the elements are copied so the value copy cost will be 8 * 100 = 800 bytes
(1 int
is 8 bytes
assuming 64-bit architectures) but when we copy a slice only the direct part is copied (len
, cap
and elements pointer
) so the value copy cost will be 8 + 8 + 8 = 24 bytes
. Even though bough slice and array contain 100 elements the value copy cost of the array is way bigger than that of the slice.
From the above scenario the performance concerns only affects arrays and not slices, I'll be focusing on how to use arrays with performance in mind. Also for small array sizes these performances differences are negligible and not worth the effort to make things faster
The only thing here is not using range
key word to loop over arrays
package main
import "fmt"
func main() {
// Don't do this
arr := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
<span class="c">// arr is copied</span>
<span class="k">for</span> <span class="n">key</span><span class="p">,</span> <span class="n">value</span> <span class="o">:=</span> <span class="k">range</span> <span class="n">arr</span> <span class="p">{</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="n">key</span><span class="p">,</span> <span class="n">value</span><span class="p">)</span>
<span class="p">}</span>
<span class="c">// Do this instead</span>
<span class="k">for</span> <span class="n">i</span> <span class="o">:=</span> <span class="m">0</span><span class="p">;</span> <span class="n">i</span> <span class="o"><</span> <span class="nb">len</span><span class="p">(</span><span class="n">arr</span><span class="p">);</span> <span class="n">i</span><span class="o">++</span> <span class="p">{</span>
<span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="n">i</span><span class="p">,</span> <span class="n">arr</span><span class="p">[</span><span class="n">i</span><span class="p">])</span>
<span class="p">}</span>
}
Conclusions
If you have any questions, ask them in the comments and I'll be happy to answer them, if you something you want me to
write on write in the comments.
Top comments (13)
Great in depth discussion on a less understood aspect of Go. Thanks!
Thank you so much for your kind words
very helpful
Re "Potential Quirks of Slices", I think you may be wrong about
regexp.Find
holding onto memory the way you describe. This used to be a problem, and if you read an earlier Go blog post about slices, you may think it still is a problem. But it hasn't been a problem for a while.The old version of
regexp.Find
looked like this (note the last line especially):The new version looks like this (again, the last line is key):
Because the new version uses a full slice expression, I don't think that it holds onto memory any longer. The third item in a full slice expression specifies the maximum capacity of the slice. The definition states that the capacity of the resulting slice is no greater (in this case) than
a[1] - a[0]
. That means that the capacity will be no longer than the length of the string found, in your casegopher
.Full slice expressions were introduced in December of 2013, and
regexp.Find
was fixed sometime after that. This is great to know about because we can also use full slice expressions to control how much of a backing array is exposed by a slice. This can protect us from blocking the garbage collector, as here, and it can also prevent the caller from being able to (accidentally or intentionally) changing parts of the backing array that they shouldn't have access to. (That is, in the old version ofregexp.Find
, the calling code would have access to a lot more of the 1 GB document in your example than just the word "gopher." In the new version, that's protected against.)I did not know that
Thanks for the correction!
I did not quite understand this.
Even full slicing doesnt do a deep copy. It still exposes the same underlying array.
For instance, int the following program , editing s would also edit the b1 slice.
I am using go 1.23 version.
Error on line 14 of codeblock 11
Should read
Thanks for catching that.
Hi! Nice article. Just a comment: "len is a built-in function that returns the number of items in an array of slice" shouldn't be "in an array or slice"?
Uh oh... this part might need revising:
Hi, the result is 3,4
cap(subSlice) = 4
i my case fmt.Println(len(subSlice), cap(subSlice)), gives 3 5. Looks like the subSlice is retaining the slice length, when we ommit it during creation
very helpful. Thanks!