Home/Blog/Programming/Contextualizing slices in Go
Home/Blog/Programming/Contextualizing slices in Go

Contextualizing slices in Go

Jun 09, 2023
7 min read

Become a Software Engineer in Months, Not Years

From your first line of code, to your first day on the job — Educative has you covered. Join 2M+ developers learning in-demand programming skills.

Go is a high-level language with low-level language features. It was designed by Robert Griesemer, Rob Pike, and Ken Thompson and released by Google in 2012. Go supports high-level language constructs and characteristics, such as garbage collectionProcess of freeing up (or collecting) unused memory from the heap, Golang generics, high-performance networking, and Go concurrency. At the same time, it offers implementation and control that has low-level characteristics. For example, it provides high-level concurrency support through goroutines and also through channels, but low-level functionality can also be used with the sync/atomic package to enforce thread-safe synchronization. It is statically typed, fully compiled, and hence offers better runtime efficiency (compared to Java, Python, JavaScript, and other interpretable languages).

Go is an open-source programming language with a growing market demand. It’s a good choice for fast, efficient, and reliable software development with a quick learning curve.

The Go language has a special data structure called a slice that provides a powerful and efficient way to represent arrays due to its dynamic size and flexibility.

Slices in Go language#

A slice is an array with a variable length that can store the same type of elements. Internally, a slice is a reference to an underlying array. A slice always starts with 0 index, and the last element of a slice is at N-1 index where N is the length of the slice. The size C of the underlying array is known as the capacity of the slice where C is always greater than or equal to N.

Slice, pointer, and array: A pictorial explanation#

Go can be viewed as a C-family language and provides the support for pointers and arrays. However, for safety reasons, there are restrictions on Go pointers (when compared with C pointers). In addition, Go supports slices, which are flexible and dynamic.

The following diagram shows a pictorial comparison of an array, a pointer, and a slice. The memory layout of a slice in Go is a structure that consists of a pointer to an array and two variables (len and cap indicating the length and the capacity of a slice, respectively). A pointer holds the memory address of a value. The type *T is a pointer to the value T. While the length of an array is fixed, the slice and the pointer may point to any element of the array (memory address). While the pointer does not have a particular size or capacity associated, the memory layout of a slice in Go contains variables to hold the length and the capacity of the slice.

Diagram of a slice, pointer, and array
Diagram of a slice, pointer, and array

Slice vs. array#

Array size must be defined while defining an array variable or else [...] must be used in array declaration to let the Go compiler determine it automatically. If nothing is put in the square brackets, then a slice is created. Valid indices for both the arrays and slices are from 0 and l-1 where l is the length of an array or a slice. Moreover, the size of an array can't be changed after creation, however a slice in this context is flexible.

Another difference arises while dealing with function arguments. All function arguments are passed by value in Go language. Therefore, while passing an array to a function, a copy of that array is created and passed to the function. Consequently, changes to the array done inside the function are lost on function's return. On the contrary, as mentioned above, a slice is a header containing pointer to an underlying array. Therefore, while passing a slice to a function, copy of that header is made and passed to the function. Hence, argument passing using a slice is faster and also preserves any changes performed in the function on the original slice.

Therefore, given this flexibility and power of a slice, a slice is usually preferable to an array while programming in Go. However, arrays are still useful in Go when a detailed memory layout is needed. For example, while reading a .wav (audio) file, there's an informational header with a fixed structure. Many fields in this header are usually of no concern in the implementation. While committing a write/read in this type of file in Go, a structure containing the desired fields is defined. In between, arrays of an appropriate size are defined to take care of unimportant fields in the audio file header.

Slice vs. pointer#

A pointer is represented by *T where T is the type of the stored value. For example in ptr *int, the ptr is a pointer to an int. The * operator[object Object] dereferences the pointer while the & operator provides the address of the variable (which is also a pointer to the variable).

While function arguments are passed by value in Go, pointers are one of the ways to avoid copy overhead for passing structures and arrays. Additionally, pointers to structures are handy while creating data structures like linked lists and binary trees. For further clarity about internal layout of a pointer in Go language, please refer to the figure and the discussion in this Go language lesson. However, Go doesn't support the pointer arithmetics that are available in C. Compared with pointers, slices are passed to a function without the need to use a pointer since Go passes the pointer to the underlying array of a slice internally.

Connection between a slice and an array#

Each slice is based on an underlying array with a length that's the same as the capacity of the slice. If an existing array is connected with a slice, any changes to the slice affect the referenced array. However, when the capacity of the slice changes, contents of the old underlying array are copied to a new array with the updated capacity, and the slice is pointed to the new array. For a better understanding, please refer to the coding example in this Go language lesson.

A brief walk through slice#

A slice can be created in the following different ways:

  1. With an empty slice using var keyword as follows: var aSlice []datatype.

  2. With an array syntax without specifying the array size to declare a slice. For example, aSliceArray := []datatype {value1, value2, ..., valueN}.

  3. By slicing an existing array to define a new slice. For example,
    anArray := [length]datatype {value1, value2, ..., valueN}
    aSliceArray := anArray[i1:i2], i1, and i2 are the desired indices here.

  4. With a slice using new keyword. For example, aSliceArray := new([capacity] datatype)[0:length].

  5. By declaring a slice using the make() function. For example, aSliceArray := make([]datatype, length, capacity).

Note: For cases 3–5 above, values of length and capacity must be defined in the code at the time of declaration.

In the following code widget, lines 10–24 show different ways to create (declare or initialize) an int type slice. In addition, line 31 shows that slices of other types (e.g., string) can also be defined. The function reflect.ValueOf(var).Kind() in lines 32 and 40 returns the name of data type kind (slice). Functions len(var) and cap(var) in lines 32 and 41 return the length and the capacity of the sliceLength of a slice is the number of elements in a slice. Capacity of a slice is the number of elements in the underlying array, counting from the first element in the slice.. The == operator can be used to determine whether a slice is nil or not (see lines 33–34). However, comparing two slices using the == operator will result in an error. (Please run the following code widget again after uncommenting line 36.)

package main
import (
"fmt"
"reflect"
)
func main() {
//Creating and using an empty slice
var intSlice []int
printSlice(intSlice)
// A slice using array
array := [4]int {0, 5, 10, 15}
aSlice1 := array[0:4]
// A slice using array syntax without specifying array size
aSlice2 := []int {1, 6, 11, 16}
// A slice using new keyword
aSlice3 := new([10]int)[0:4]
// A slice using the make() function
aSlice4 := make([]int, 4, 10)
printSlice(aSlice1)
printSlice(aSlice2)
printSlice(aSlice3)
printSlice(aSlice4)
var aSlice5 = []string{"Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"}
fmt.Printf("%s %s len=%v cap:%v\n", "string-type", reflect.ValueOf(aSlice5).Kind(), len(aSlice5), cap(aSlice5))
fmt.Println(intSlice == nil)
fmt.Println(aSlice1 == nil)
//Uncomment following to see error: Comparing two slices using ==
//fmt.Println(aSlice1 == intSlice)
}
func printSlice(s []int) {
fmt.Print(reflect.ValueOf(s).Kind())
fmt.Printf(" len=%d cap=%d %v\n", len(s), cap(s), s)
}

After slice creation, different slice operations can be used to add, access, and change the slice elements. Following code widget shows the syntax for access and modification operations (lines 12–13) on an int slice. Lines 17–19 demonstrate the addition of new elements in a slice using the append() method. However, there's no built-in method in Go language for item removal from a slice for optimization reasons. This task is achieved by defining a function remove_element(intArray []int, index int) []int in lines 25–27 that intelligently uses a built-in append() method.

package main
import (
"fmt"
// "reflect"
)
func main() {
//Add, access, change, and append slice elements
aSliceArray := make([]int, 2, 4)
aSliceArray[0] = 1
aSliceArray[1] = 2
fmt.Println("Slice contents:", aSliceArray)
fmt.Printf("len=%d cap=%d\n", len(aSliceArray), cap(aSliceArray))
aSliceArray = append(aSliceArray, 3, 4, 5, 6, 7)
fmt.Println("Slice after append():", aSliceArray)
fmt.Printf("len=%d cap=%d\n", len(aSliceArray), cap(aSliceArray))
aSliceArray = remove_element(aSliceArray, 3)
fmt.Printf("Slice after removal: %d \nlen=%d cap=%d\n", aSliceArray, len(aSliceArray), cap(aSliceArray))
}
func remove_element(intArray []int, index int) []int {
return append(intArray[:index], intArray[index+1:]...)
}

Wrapping up and next steps#

In brief, Go is an ideal programming language to learn with so many attractive features. If you are interested in exploring further resources, please refer to the following courses and don't forget to share your feedback and opinions with us!

Grokking Coding Interview Patterns in Go

Cover
Grokking the Coding Interview Patterns

With thousands of potential questions to account for, preparing for the coding interview can feel like an impossible challenge. Yet with a strategic approach, coding interview prep doesn’t have to take more than a few weeks. Stop drilling endless sets of practice problems, and prepare more efficiently by learning coding interview patterns. This course teaches you the underlying patterns behind common coding interview questions. By learning these essential patterns, you will be able to unpack and answer any problem the right way — just by assessing the problem statement. This approach was created by FAANG hiring managers to help you prepare for the typical rounds of interviews at major tech companies like Apple, Google, Meta, Microsoft, and Amazon. Before long, you will have the skills you need to unlock even the most challenging questions, grok the coding interview, and level up your career with confidence. This course is also available in JavaScript, Python, Go, and C++ — with more coming soon!

85hrs
Intermediate
269 Challenges
270 Quizzes

Go Brain Teasers

Cover
Go Brain Teasers

Who knew puzzles could be fun? Puzzles and brain teasers are a great way to practice your critical thinking and improve your logic and problem-solving skills. This course provides a basic understanding of Go. The teasers in this course will help you practice what you already know and help avoid future mistakes.

2hrs 5mins
Intermediate
47 Playgrounds
25 Quizzes

The Way to Go

Cover
The Way to Go

Go (sometimes called Golang) is one of the most popular languages today, and is a key part of many enterprise tech stacks. Many developers prefer Go to other languages like C++ and Scala because of its memory management model which allows for easier concurrency. In this course, you will learn the core constructs and techniques of the language. After going through the basics, you will then learn more advanced Go concepts like error-handling, networking, and templating. You'll learn how to program efficiently in Go by gaining knowledge of common pitfalls and patterns, as well as by building your own applications.

25hrs
Intermediate
19 Challenges
11 Quizzes

Building a Backend Application with Go

Cover
Building a Backend Application with Go

In modern backend applications, many programming languages can be chosen, including Go—a programming language that provides high performance with concise syntax and can also be used to develop backend applications, including REST API and GraphQL applications. This course will cover many tools that can be used to develop a backend application, including fiber for REST API application, gqlgen for GraphQL application, and apitest for testing purposes. In this course, you’ll learn about the main concepts of web applications, including HTTP, REST API, databases, and GraphQL. You’ll cover how to develop a backend application and deploy the application as well. By the end of the course, you’ll be able to develop a backend application using REST API and GraphQL approach. Also, you’ll be able to test and deploy the backend application that has been developed.

13hrs
Beginner
31 Playgrounds
3 Quizzes

  

Free Resources