Solomon Esenyi Python/Golang developer and Technical Writer with a passion for open-source, cryptography, and serverless technologies.

A comprehensive guide to data structures in Go

10 min read 2909

You’ve probably heard of data structures and have used them in other programming languages, but do you know how to use them in Go?

As one of the fastest-growing programming languages in the industry, it is important for devs to understand how to utilize this vital feature in order to create scalable, reliable applications.

In this article, we will be covering data structures in Go, and taking a deep dive into concepts like arrays, slices, maps, and structs. Plus, I’ll provide multiple code examples along the way.

Prerequisites

To follow and understand this tutorial, you will need the following:

Arrays

An array is a collection of data of a specific type. It stores multiple values in a single variable where each element has an index to reference itself.

Arrays come in handy when you need to keep more than one thing in a single location, like a list of people who attended an event or the age of students in a class.

Creating an array

To create an array, we need to define its name, length, and type of values we will be storing:

var studentsAge [10]int

In this code blog, we created an array named studentsAge, which can store a maximum of ten int values.

Creating an array from literals

You can create an array from literals, meaning you’re assigning values to them at the point of creation.

Let’s see how it can be used:

// creating an array and assigning values later
var studentsAge [10]int
studentsAge = [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

// creating and assigning values to an array
var studentsAge = [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

// creating and assigning values to an array without var keyword
studentsAge := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

Creating an array of an array

You can create an array where every element is an individual array (nested arrays), like so:

// creating a nested array
nestedArray := \[3\][5]int{
  {1, 2, 3, 4, 5},
  {6, 7, 8, 9, 10},
  {11, 12, 13, 14, 15},
}
fmt.Println(nestedArray) // \[[1 2 3 4 5\] [6 7 8 9 10] [11 12 13 14 15]]

Accessing the values in an array

Each element in an array has an index that you can use to access and modify its value. The index of an array is always an integer and starts counting from zero:

// creating an array of integers
studentsAge := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

// accessing array values with their indexes
fmt.Println(studentsAge[0]) // 1
fmt.Println(studentsAge[1]) // 2
fmt.Println(studentsAge[9]) // 10

// using a for loop to access an array
for i := 0; i < 10; i++ {
  fmt.Println(studentsAge[i])
}

// using range to access an array
for index, value := range studentsAge {
  fmt.Println(index, value)
}

Modifying the values in an array

Arrays are a mutable data structures, so it is possible to modify their values after creation:

// creating an array of integers
studentsAge := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

// modifying array values with their indexes
studentsAge[0] = 5
studentsAge[4] = 15
studentsAge[7] = 10

fmt.Println(studentsAge) // [5 2 3 4 15 6 7 10 9 10]

Getting the length of an array

Go provides a len function that you can use to get the length of an array.

Let’s see how it can be used:

// creating and getting the length of an array with a length of 10
var arrayOfIntegers [10]int
fmt.Println(len(arrayOfIntegers)) // 10

// creating and getting the length of an array with a length of 7
var arrayOfStrings [7]string
fmt.Println(len(arrayOfStrings)) // 7

// creating and getting the length of an array with a length of 20
var arrayOfBooleans [20]bool
fmt.Println(len(arrayOfBooleans)) // 20

Note that it is impossible to change the length of an array because it becomes part of the type during creation.

Slices

Like arrays, slices allow you to store multiple values of the same type in a single variable and access them with indexes. The main difference between slices and arrays is that slices have dynamic lengths, while arrays are fixed.



Creating a slice

To create a slice, we need to define its name and the type of values we will be storing:

var sliceOfIntegers []int

We created a slice named sliceOfIntegers, which stores int values.

Creating a slice from an array

In its original form, a slice is an extracted portion of an array. To create a slice from an array, we need to provide Go with the part to extract.

Let’s see how to do so:

// creating an array of integers
studentsAge := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

// creating slices from arrays
fiveStudents := studentsAge[0:5]
fmt.Println(fiveStudents) // [1 2 3 4 5]
threeStudents := studentsAge[3:6]
fmt.Println(threeStudents) // [4 5 6]

The slicing format requires you to provide the indexes to start and stop the Go slice extraction. If any of the parameters are omitted, Go uses zero as the starting point (beginning of the array) and the array’s length if the ending is omitted:

// creating an array of integers
studentsAge := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

// creating slices from arrays
fmt.Println(studentsAge[:4]) // [1 2 3 4]
fmt.Println(studentsAge[6:]) // [7 8 9 10]
fmt.Println(studentsAge[:])  // [1 2 3 4 5 6 7 8 9 10]

It is also possible to create slices from other slices with the same format as arrays:

// creating an array of integers
studentsAge := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

// creating slices from arrays
firstSlice := studentsAge[:8]
fmt.Println(firstSlice) // [1 2 3 4 5 6 7 8]

// creating slices from slices
secondSlice := firstSlice[1:5]
fmt.Println(secondSlice) // [2 3 4 5]

Creating a slice with make

Go provides a make function that you can use to create slices by specifying their length. After creation, Go will fill the slice with the zero value of its type:

// creating slices with make specifying length
sliceOfIntegers := make([]int, 5)  // [0 0 0 0 0]
sliceOfBooleans := make([]bool, 3) // [false false false]

Every slice has a length and a capacity. The length of the slice is the number of elements in the slice, while the capacity is the number of elements in the underlying array, counted from the first element in the slice.

The make function allows us to create a slice with a specified capacity. Here’s the usage:

// creating a slice with a length of 5 and a capacity of 10
sliceOfStrings := make([]string, 5, 10)

Creating a slice from literals

You can create a slice from literals, meaning you’re assigning values to them at the point of creation:

// creating a slice and assigning values later
var tasksRemaining []string
tasksRemaining = []string{"task 1", "task 2", "task 3"}

// creating and assigning values to a slice
var tasksRemaining = []string{"task 1", "task 2", "task 3"}

// creating and assigning values to a slice without var keyword
tasksRemaining := []string{"task 1", "task 2", "task 3"}

Creating a slice of a slice

You can create a slice in which every element is an individual slice (nested slices), like so:

// creating a nested slice
nestedSlice := [][]int{
  {1},
  {2, 3},
  {4, 5, 6},
  {7, 8, 9, 10},
}
fmt.Println(nestedSlice) // \[[1\] [2 3] \[4 5 6\] [7 8 9 10]]

Accessing and modifying the values in a slice

Each element in a slice has an index that you can use to access and modify its value. The index of a slice is always an integer and starts counting from zero:

// creating a slice from literals
sliceOfIntegers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

// accessing slice values with their indexes
firstInteger := sliceOfIntegers[0]  // 1
secondInteger := sliceOfIntegers[1] // 2
lastInteger := sliceOfIntegers[9]   // 10

// using a for loop to access a slice
for i := 0; i < 10; i++ {
  fmt.Println(sliceOfIntegers[i])
}

// using range to access a slice
for index, value := range sliceOfIntegers {
  fmt.Println(index, value)
}

Slices are mutable data structures, so it is possible to modify their values after creation:

// creating a slice from literals
sliceOfIntegers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

sliceOfIntegers[0] = 3
sliceOfIntegers[5] = 2
sliceOfIntegers[9] = -10

fmt.Println(sliceOfIntegers) // [3 2 3 4 5 2 7 8 9 -10]

Getting the length and capacity of a slice

Go provides a len function that you can use to get the length of a slice:

// creating and getting the length of a slice
sliceOfIntegers := make([]int, 10)
fmt.Println(len(sliceOfIntegers)) // 10

There is also the cap function, which you can use to get the capacity of a slice:

// creating and getting the capacity of a slice
sliceOfIntegers := make([]int, 10, 15)
fmt.Println(cap(sliceOfIntegers)) // 15

Adding elements to a slice

Go provides an append function that you can use to add elements to an existing slice:

// creating a slice from literals
sliceOfIntegers := []int{1, 2, 3}

// using append to add a single value to the slice
sliceOfIntegers = append(sliceOfIntegers, 4)
fmt.Println(sliceOfIntegers) // [1 2 3 4]

// using append to add multiple values to the slice
sliceOfIntegers = append(sliceOfIntegers, 5, 6, 7)
fmt.Println(sliceOfIntegers) // [1 2 3 4 5 6 7]

// using append to add a slice to a slice
anotherSlice := []int{8, 9, 10}
sliceOfIntegers = append(sliceOfIntegers, anotherSlice...)
fmt.Println(sliceOfIntegers) // [1 2 3 4 5 6 7 8 9 10]

The append function is variadic and accepts a variable number of arguments. This is why we can pass multiple values to it by separating them with commas.

Maps

A map is a data structure that assigns keys to its values (key-value pairs). It is similar to Objects in JavaScript, HashMap in Java, and Dictionaries in Python. The zero value of a map is nil.

Creating a map

To create a map, we need to define its name and the data type for its keys and values:

var studentsAge map[string]int

Here, we created a map named studentsAges, which stores its keys as strings and values as ints.

Initializing and creating maps with make

Go provides a make function that you can use to initialize maps you have created:

// creating a string -> int map
var studentsAge map[string]int
studentsAge = make(map[string]int)

Maps must be initialized with make after their creation before assigning values to them.

You can also create maps with make. Doing so doesn’t require you to initialize it again before using:

// creating a string -> int map
studentsAge := make(map[string]int)

Creating maps from literals

Creating a map from literals means assigning their keys and values at the point of creation. Let’s see how it can be used:

// creating a map from literals
studentsAge := map[string]int{
  "solomon": 19,
  "john":    20,
  "janet":   15,
  "daniel":  16,
  "mary":    18,
}

fmt.Println(studentsAge) // map[daniel:16 janet:15 john:20 mary:18 solomon:19]

Creating a map of maps

You can create a map where every key references another map (nested maps), like so:

// creating nested maps
studentResults := map[string]map[string]int{
  "solomon": {"maths": 80, "english": 70},
  "mary":    {"maths": 74, "english": 90},
}

fmt.Println(studentResults) // map[mary:map[english:90 maths:74] solomon:map[english:70 maths:80]]
fmt.Println(studentResults["solomon"]) // map[english:70 maths:80]
fmt.Println(studentResults\["solomon"\]["maths"]) // 80

In this code block, we created a map with string keys, and each value is another map with string keys and int values.

Adding and accessing values in a map

To add values to a map, you need to assign the key to whichever value you want it to be:

// creating a string -> int map
studentsAge := make(map[string]int)

// adding values to the map
studentsAge["solomon"] = 19
studentsAge["john"] = 20
studentsAge["janet"] = 15

fmt.Println(studentsAge) // map[janet:15 john:20 solomon:19]

To access values in a map, you need to reference the assigned key:

// creating a map from literals
studentsAge := map[string]int{
  "solomon": 19,
  "john":    20,
  "janet":   15,
  "daniel":  16,
  "mary":    18,
}

// accessing values in the map
fmt.Println(studentsAge["solomon"]) // 19
fmt.Println(studentsAge["mary"])    // 18
fmt.Println(studentsAge["daniel"])  // 16

Checking key existence in a map

There are times when you want to check if a key already exists in a map. Go allows you do this with a two-value assignment to the map value:

// creating a map from literals
studentsAge := map[string]int{
  "solomon": 19,
  "john":    20,
  "janet":   15,
  "daniel":  16,
  "mary":    18,
}

// two-value assignment to get an existing key
element, ok := studentsAge["solomon"]
fmt.Println(element, ok) // 19 true

// two-value assignment to get a non-existing key
element, ok = studentsAge["joel"]
fmt.Println(element, ok) // 0 false

When a two-value assignment is used to access values in a map, the first value returned is the value of the key in the map, while the second variable is a boolean indicating if the key exists or not.

If the key does not exist, the first value is assigned to the zero value of the map value type.

Updating values in a map

To update values in a map, you need to reference an existing key and assign a new value to it:

// creating a map from literals
studentsAge := map[string]int{
  "solomon": 19,
  "john":    20,
  "janet":   15,
  "daniel":  16,
  "mary":    18,
}

// updating values in the map
studentsAge["solomon"] = 20
fmt.Println(studentsAge["solomon"]) // 20

// updating values in the map
studentsAge["mary"] = 25
fmt.Println(studentsAge["mary"]) // 25

Deleting keys from maps

Go provides a delete function that you can use to remove keys from an existing map:

// creating a map from literals
studentsAge := map[string]int{
  "solomon": 19,
  "john":    20,
  "janet":   15,
  "daniel":  16,
  "mary":    18,
}
fmt.Println(studentsAge) // map[daniel:16 janet:15 john:20 mary:18 solomon:19]

// deleting keys from the studentsAge map
delete(studentsAge, "solomon")
delete(studentsAge, "daniel")

fmt.Println(studentsAge) // map[janet:15 john:20 mary:18]

Structs

A struct is a collection of data fields with defined data types. Structs are similar to classes in OOP languages, in that they allow developers to create custom data types that hold and pass complex data structures around their systems.

Creating a struct

To create a struct, we will use the type keyword in Go, then define its name and data fields with their respective data types:

type Rectangle struct {
  length  float64
  breadth float64
}

We created a struct named Rectangle with length and breadth data fields of type float64.

Structs are types themselves, so when creating them with the type keyword, they must be made directly under a package declaration and not inside functions like main.

Creating struct instances

To create an instance, we need to define its name, data type for its keys, and data type for its values:

// creating a struct instance with var
var myRectangle Rectangle

// creating an empty struct instance
myRectangle := Rectangle{}

Creating struct instances from literals

You can create a struct instance from literals, meaning you’re assigning their field values to them at the point of creation:

// creating a struct instance specifying values
myRectangle := Rectangle{10, 5}

// creating a struct instance specifying fields and values
myRectangle := Rectangle{length: 10, breadth: 5}

// you can also omit struct fields during their instantiation
myRectangle := Rectangle{breadth: 10}

If you omit a struct field during instantiation, it will default to the type’s zero value.

Creating an array and slice of structs

Because structs are data types, it is possible to create arrays and slices of them, like so:

arrayOfRectangles := [5]Rectangle{
  {10, 5},
  {15, 10},
  {20, 15},
  {25, 20},
  {30, 25},
}
fmt.Println(arrayOfRectangles) // [{10 5} {15 10} {20 15} {25 20} {30 25}]

sliceOfRectangles := []Rectangle{
  {10, 5},
  {15, 10},
  {20, 15},
  {25, 20},
  {30, 25},
}
fmt.Println(sliceOfRectangles) // [{10 5} {15 10} {20 15} {25 20} {30 25}]

Creating a pointer struct instance

Go also allows creation of struct instances that are pointers to the struct definition:

// creating a pointer struct instance
myRectangle := &Rectangle{length: 10, breadth: 5}
fmt.Println(myRectangle, *myRectangle) // &{10 5} {10 5}

You can also create a pointer struct instance with new. Let’s see how:

// creating a struct instance with new
myRectangle := new(Rectangle)
fmt.Println(myRectangle, *myRectangle) // &{0 0} {0 0}

Accessing and updating struct field values

To access fields in a struct, you need to reference the field name:

// creating a struct instance specifying fields and values
myRectangle := Rectangle{length: 10, breadth: 5}

// accessing the values in struct fields
fmt.Println(myRectangle.length)  // 10
fmt.Println(myRectangle.breadth) // 5

To update values in a struct field, you need to reference the field name and assign a new value to it:

// creating a struct instance specifying fields and values
myRectangle := Rectangle{length: 10, breadth: 5}
fmt.Println(myRectangle) // {10 5}

myRectangle.length = 20
myRectangle.breadth = 8
fmt.Println(myRectangle) // {20 8}

Nesting a struct in a struct

Go allows you to use structs as data fields in another struct (nested structs):

// creating a nested struct
type address struct {
  houseNumber int
  streetName  string
  city        string
  state       string
  country     string
}

type Person struct {
  firstName   string
  lastName    string
  homeAddress address
}

You have to create an instance of the Person and address structs when creating a new instance of the Person struct, like so:

// creating an instance of a nested struct
person := Person{
  firstName: "Solomon",
  lastName:  "Ghost",
  homeAddress: address{
    houseNumber: 10,
    streetName:  "solomon ghost street",
    city:        "solomon city",
    state:       "solomon state",
    country:     "solomon country",
  },
}

fmt.Println(person.firstName)           // Solomon
fmt.Println(person.homeAddress.country) // solomon country

Anonymous structs

Anonymous structs allow you to create structs inside functions and use them on the go. Let’s see how it can be used:

// creating a struct anonymously
circle := struct {
  radius float64
  color  string
}{
  radius: 10.6,
  color:  "green",
}

fmt.Println(circle)       // {10.6 green}
fmt.Println(circle.color) // green

Creating struct methods

Struct methods are functions that are attached to a struct. They can only be called via a struct instance and automatically receive the struct instance as parameters.

To create a struct method, we need to define the struct it will be attached to, its name, parameters (if any), and return types (if any). Let’s see it in action:

type Rectangle struct {
  length  float64
  breadth float64
}

func (r Rectangle) area() float64 {
  return r.length * r.breadth
}

Here, we created an area method for our Rectangle struct, which uses field values to calculate and return the shape’s area as float64. We can proceed to use this in code like so:

// creating a struct instance
myRectangle := Rectangle{10, 5}

// calling the Rectangle area method
fmt.Println(myRectangle.area()) // 50

Updating struct field values with methods

Structs pass a copy of their instances to methods, so these changes will not reflect if you were to update the value of the fields in the method.

However, there may be cases in which you want to update field values from methods. Go allows methods receive a pointer reference instead of the value itself:

func (r *Rectangle) setLength(length float64) {
  r.length = length
}

func (r *Rectangle) setBreadth(breadth float64) {
  r.breadth = breadth
}

We created a setLength and setBreadth method for our Rectangle struct that updates the field variables with arguments we pass to it. We can proceed to use this in code like so:

// creating a struct instance
myRectangle := Rectangle{10, 5}
fmt.Println(myRectangle) // {10 5}

// calling the modifier methods on our instance
myRectangle.setLength(20)
myRectangle.setBreadth(10)
fmt.Println(myRectangle) // {20 10}

Conclusion

In this article, we learned about the various data structures in Go like arrays, slices, maps, and structs. We also showed multiple code examples, use cases, and functions.

I hope this was a useful guide to what can often be a complicated topic. With this article as a reference guide, you can confidently use the correct data structures for your use case and create fast, performant apps.

If you like, head over to the Tour of Go for more references and examples of Go data structures.

Get setup with LogRocket's modern error tracking in minutes:

  1. Visit https://logrocket.com/signup/ to get an app ID.
  2. Install LogRocket via NPM or script tag. LogRocket.init() must be called client-side, not server-side.
  3. $ npm i --save logrocket 

    // Code:

    import LogRocket from 'logrocket';
    LogRocket.init('app/id');
    Add to your HTML:

    <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
    <script>window.LogRocket && window.LogRocket.init('app/id');</script>
  4. (Optional) Install plugins for deeper integrations with your stack:
    • Redux middleware
    • ngrx middleware
    • Vuex plugin
Get started now
Solomon Esenyi Python/Golang developer and Technical Writer with a passion for open-source, cryptography, and serverless technologies.

Leave a Reply