Introduction to Slices in Golang

Introduction to Slices in Golang
Introduction to Slices in Golang

A Slice is a segment of an array. Slices build on arrays and provide more power, flexibility, and convenience compared to arrays.

Just like arrays, Slices are indexable and have a length. But unlike arrays, they can be resized.

Internally, A Slice is just a reference to an underlying array. In this article, we’ll learn how to create and use slices, and also understand how they work under the hood.

Declaring a Slice

A slice of type T is declared using []T . For example, Here is how you can declare a slice of type int -

// Slice of type `int`
var s []int

The slice is declared just like an array except that we do not specify any size in the brackets [] .

Creating and Initializing a Slice

1. Creating a slice using a slice literal

You can create a slice using a slice literal like this -

// Creating a slice using a slice literal
var s = []int{3, 5, 7, 9, 11, 13, 17}

The expression on the right-hand side of the above statement is a slice literal. The slice literal is declared just like anarray literal, except that you do not specify any size in the square brackets [] .

When you create a slice using a slice literal, it first creates an array and then returns a slice reference to it.

Let’s see a complete example -

package main
import "fmt"

func main() {
 // Creating a slice using a slice literal
 var s = []int{3, 5, 7, 9, 11, 13, 17}  // Creates an array, and returns a slice reference to the array

 // Short hand declaration
 t := []int{2, 4, 8, 16, 32, 64}

 fmt.Println("s = ", s)
 fmt.Println("t = ", t)
}
# Output
s =  [3 5 7 9 11 13 17]
t =  [2 4 8 16 32 64]

2. Creating a slice from an array

Since a slice is a segment of an array, we can create a slice from an array.

To create a slice from an array a , we specify two indices low (lower bound) and high (upper bound) separated by a colon -

// Obtaining a slice from an array `a`
a[low:high]

The above expression selects a slice from the array a . The resulting slice includes all the elements starting from index low to high , but excluding the element at index high .

Let’s see an example to make things more clear -

package main
import "fmt"

func main() {
 var a = [5]string{"Alpha", "Beta", "Gamma", "Delta", "Epsilon"}

 // Creating a slice from the array
 var s []string = a[1:4]

 fmt.Println("Array a = ", a)
 fmt.Println("Slice s = ", s)
}
Array a =  [Alpha Beta Gamma Delta Epsilon]
Slice s =  [Beta Gamma Delta]

The low and high indices in the slice expression are optional. The default value for low is 0 , and high is the length of the slice.

package main
import "fmt"

func main() {
 var a = [5]string{"C", "C++", "Java", "Python", "Go"}

 var slice1 = a[1:4]
 var slice2 = a[:3]
 var slice3 = a[2:]
 var slice4 = a[:]

 fmt.Println("Array a = ", a)
 fmt.Println("slice1 = ", slice1)
 fmt.Println("slice2 = ", slice2)
 fmt.Println("slice3 = ", slice3)
 fmt.Println("slice4 = ", slice4)
}
# Output
Array a =  [C C++ Java Python Go]
slice1 =  [C++ Java Python]
slice2 =  [C C++ Java]
slice3 =  [Java Python Go]
slice4 =  [C C++ Java Python Go]

3. Creating a slice from another slice

A slice can also be created by slicing an existing slice.

package main
import "fmt"

func main() {
 var cities = []string{"New York", "London", "Chicago", "Beijing", "Delhi", "Mumbai", "Bangalore", "Hyderabad", "Hong Kong"}

 var asianCities = cities[3:]
 var indianCities = asianCities[1:5]

 fmt.Println("cities = ", cities)
 fmt.Println("asianCities = ", asianCities)
 fmt.Println("indianCities = ", indianCities)
}
# Output
cities =  [New York London Chicago Beijing Delhi Mumbai Bangalore Hyderabad Hong Kong]
asianCities =  [Beijing Delhi Mumbai Bangalore Hyderabad Hong Kong]
indianCities =  [Delhi Mumbai Bangalore Hyderabad]

Modifying a slice

Slices are reference types. They refer to an underlying array. Modifying the elements of a slice will modify the corresponding elements in the referenced array. Other slices that refer the same array will also see those modifications.

package main
import "fmt"

func main() {
 var a = [7]string{"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}

 var slice1 = a[1:]
 var slice2 = a[3:]

 fmt.Println("------- Before Modifications -------")
 fmt.Println("a  = ", a)
 fmt.Println("slice1 = ", slice1)
 fmt.Println("slice2 = ", slice2)

 slice1[0] = "TUE"
 slice1[1] = "WED"
 slice1[2] = "THU"

 slice2[1] = "FRIDAY"

 fmt.Println("\n-------- After Modifications --------")
 fmt.Println("a  = ", a)
 fmt.Println("slice1 = ", slice1)
 fmt.Println("slice2 = ", slice2)
}
# Output
------- Before Modifications -------
a  =  [Mon Tue Wed Thu Fri Sat Sun]
slice1 =  [Tue Wed Thu Fri Sat Sun]
slice2 =  [Thu Fri Sat Sun]

-------- After Modifications --------
a  =  [Mon TUE WED THU FRIDAY Sat Sun]
slice1 =  [TUE WED THU FRIDAY Sat Sun]
slice2 =  [THU FRIDAY Sat Sun]

Length and Capacity of a Slice

A slice consists of three things -

  • A pointer (reference) to an underlying array.
  • The length of the segment of the array that the slice contains.
  • The capacity (the maximum size up to which the segment can grow).
Introduction to Slices in Golang
Introduction to Slices in Golang

Let’s consider the following array and the slice obtained from it as an example -

var a = [6]int{10, 20, 30, 40, 50, 60}
var s = [1:4]

Here is how the slice s in the above example is represented -

Introduction to Slices in Golang
Introduction to Slices in Golang

The length of the slice is the number of elements in the slice, which is 3 in the above example.

The capacity is the number of elements in the underlying array starting from the first element in the slice. It is 5 in the above example.

You can find the length and capacity of a slice using the built-in functions len() and cap() -

package main
import "fmt"

func main() {
 var a = [6]int{10, 20, 30, 40, 50, 60}
 var s = a[1:4]

 fmt.Printf("s = %v, len = %d, cap = %d\n", s, len(s), cap(s))
}
# Output
s = [20 30 40], len = 3, cap = 5

A slice’s length can be extended up to its capacity by re-slicing it. Any attempt to extend its length beyond the available capacity will result in a runtime error.

Check out the following example to understand how re-slicing a given slice changes its length and capacity -

package main
import "fmt"

func main() {
 var s = []int{10, 20, 30, 40, 50, 60, 70, 80, 90, 100}
 fmt.Println("Original Slice")
 fmt.Printf("s = %v, len = %d, cap = %d\n", s, len(s), cap(s))

 s = s[1:5]
 fmt.Println("\nAfter slicing from index 1 to 5")
 fmt.Printf("s = %v, len = %d, cap = %d\n", s, len(s), cap(s))

 s = s[:8]
 fmt.Println("\nAfter extending the length")
 fmt.Printf("s = %v, len = %d, cap = %d\n", s, len(s), cap(s))

 s = s[2:]
 fmt.Println("\nAfter dropping the first two elements")
 fmt.Printf("s = %v, len = %d, cap = %d\n", s, len(s), cap(s))
}
# Output
Original Slice
s = [10 20 30 40 50 60 70 80 90 100], len = 10, cap = 10

After slicing from index 1 to 5
s = [20 30 40 50], len = 4, cap = 9

After extending the length
s = [20 30 40 50 60 70 80 90], len = 8, cap = 9

After dropping the first two elements
s = [40 50 60 70 80 90], len = 6, cap = 7

Creating a slice using the built-in make() function

Now that we know about the length and capacity of a slice. Let’s look at another way to create a slice.

Golang provides a library function called make() for creating slices. Following is the signature of make() function -

func make([]T, len, cap) []T

The make function takes a type, a length, and an optional capacity. It allocates an underlying array with size equal to the given capacity, and returns a slice that refers to that array.

package main
import "fmt"

func main() {
 // Creates an array of size 10, slices it till index 5, and returns the slice reference
 var s = make([]int, 5, 10)
 fmt.Printf("s = %v, len = %d, cap = %d\n", s, len(s), cap(s))
}
# Output
s = [0 0 0 0 0], len = 5, cap = 10

The capacity parameter in the make() function is optional. When omitted, it defaults to the specified length -

package main
import "fmt"

func main() {
 // Creates an array of size 5, and returns a slice reference to it
 var s = make([]int, 5)
 fmt.Printf("s = %v, len = %d, cap = %d\n", s, len(s), cap(s))
}
# Output
s = [0 0 0 0 0], len = 5, cap = 5

Zero value of slices

Thezero value of a slice is nil . A nil slice doesn’t have any underlying array, and has a length and capacity of 0 -

package main
import "fmt"

func main() {
 var s []int
 fmt.Printf("s = %v, len = %d, cap = %d\n", s, len(s), cap(s))

 if s == nil {
  fmt.Println("s is nil")
 }
}
# Output
s = [], len = 0, cap = 0
s is nil

Slice Functions

1. The copy() function: copying a slice

The copy() function copies elements from one slice to another. Its signature looks like this -

func copy(dst, src []T) int

It takes two slices - a destination slice, and a source slice. It then copies elements from the source to the destination and returns the number of elements that are copied.

Note that the elements are copied only if the destination slice has sufficient capacity.

package main

import "fmt"

func main() {
 var src = []string{"Sublime", "VSCode", "IntelliJ", "Eclipse"}
 var dest = make([]string, 2)

 var numElementsCopied = copy(dest, src)

 fmt.Println("src = ", src)
 fmt.Println("dest = ", dest)
 fmt.Println("Number of elements copied from src to dest = ", numElementsCopied)
}
# Output
src =  [Sublime VSCode IntelliJ Eclipse]
dest =  [Sublime VSCode]
Number of elements copied from src to dest =  2

2. The append() function: appending to a slice

The append() function appends new elements at the end of a given slice. Following is the signature of append function.

func append(s []T, x ...T) []T

It takes a slice and a variable number of arguments x …T . It then returns a new slice containing all the elements from the given slice as well as the new elements.

If the given slice doesn’t have sufficient capacity to accommodate new elements then a new underlying array is allocated with bigger capacity. All the elements from the underlying array of the existing slice are copied to this new array, and then the new elements are appended.

However, if the slice has enough capacity to accommodate new elements, then the append() function re-uses its underlying array and appends new elements to the same array.

Let’s see an example to understand things better -

package main
import "fmt"

func main() {
 var slice1 = []string{"C", "C++", "Java"}
 var slice2 = append(slice1, "Python", "Ruby", "Go")

 fmt.Printf("slice1 = %v, len = %d, cap = %d\n", slice1, len(slice1), cap(slice1))
 fmt.Printf("slice2 = %v, len = %d, cap = %d\n", slice2, len(slice2), cap(slice2))

 slice1[0] = "C#"
 fmt.Println("\nslice1 = ", slice1)
 fmt.Println("slice2 = ", slice2)
}
# Output
slice1 = [C C++ Java], len = 3, cap = 3
slice2 = [C C++ Java Python Ruby Go], len = 6, cap = 6

slice1 =  [C# C++ Java]
slice2 =  [C C++ Java Python Ruby Go]

In the above example, since slice1 has capacity 3, it can’t accommodate more elements. So a new underlying array is allocated with bigger capacity when we append more elements to it.

So if you modify slice1 , slice2 won’t see those changes because it refers to a different array.

But what if slice1 had enough capacity to accommodate new elements? Well, in that case, no new array would be allocated, and the elements would be added to the same underlying array.

Also, in that case, changes to slice1 would affect slice2 as well because both would refer to the same underlying array. This is demonstrated in the following example -

package main
import "fmt"

func main() {
 var slice1 = make([]string, 3, 10)
 copy(slice1, []string{"C", "C++", "Java"})

 var slice2 = append(slice1, "Python", "Ruby", "Go")

 fmt.Printf("slice1 = %v, len = %d, cap = %d\n", slice1, len(slice1), cap(slice1))
 fmt.Printf("slice2 = %v, len = %d, cap = %d\n", slice2, len(slice2), cap(slice2))

 slice1[0] = "C#"
 fmt.Println("\nslice1 = ", slice1)
 fmt.Println("slice2 = ", slice2)
}
# Output
slice1 = [C C++ Java], len = 3, cap = 10
slice2 = [C C++ Java Python Ruby Go], len = 6, cap = 10

slice1 =  [C# C++ Java]
slice2 =  [C# C++ Java Python Ruby Go]

Appending to a nil slice

When you append values to a nil slice, it allocates a new slice and returns the reference of the new slice.

package main

import "fmt"

func main() {
 var s []string

 // Appending to a nil slice
 s = append(s, "Cat", "Dog", "Lion", "Tiger")

 fmt.Printf("s = %v, len = %d, cap = %d\n", s, len(s), cap(s))
}
# Output
s = [Cat Dog Lion Tiger], len = 4, cap = 4

Appending one slice to another

You can directly append one slice to another using the ... operator. The following example demonstrates its usage -

package main
import "fmt"

func main() {
 var slice1 = []string{"Jack", "John", "Peter"}
 var slice2 = []string{"Bill", "Mark", "Steve"}

 var slice3 = append(slice1, slice2...)

 fmt.Println("slice1 = ", slice1)
 fmt.Println("slice2 = ", slice2)
 fmt.Println("After appending slice1 & slice2 = ", slice3)
}
# Output
slice1 =  [Jack John Peter]
slice2 =  [Bill Mark Steve]
After appending slice1 & slice2 =  [Jack John Peter Bill Mark Steve]

Slice of slices

Slices can be of any type. They can also contain other slices. The example below creates a slice of slices -

package main
import "fmt"

func main() {
 var s = [][]string{
  {"James Smith", "United States"},
  {"Maria Gracia", "England"},
  {"Sarah Johnson", "France"},
 }

 fmt.Println("Slice s = ", s)
 fmt.Println("length = ", len(s))
 fmt.Println("capacity = ", cap(s))
}
# Output
Slice s =  [[James Smith United States] [Maria Gracia England] [Sarah Johnson France]]
length =  3
capacity =  3

Conclusion

That’s all folks! In this article, you learned how to create slices, how slices work internally, and how to use the built-in functions copy() and append() to grow slices.

In the next article, we’ll learn about another very useful data structure - Maps.

Thanks for reading. See you in the next post.