Slices and Maps in Go

Rajesh GurajalaRajesh Gurajala
16 min read

Arrays in Go

Arrays are fixed-size groups of variables of the same type.

For example, [4]string is an array of 4 values of type string.

To declare an array of 10 integers:

var myInts [10]int
primes := [6]int{2, 3, 5, 7, 11, 13}            // or to declare an initialized literal:

Another Example :

package main
import "fmt"

func getm(a, b, c string) ([3]string, [3]int){
    return [3]string{a,b,c}, [3]int{len(a), len(a+b), len(a+b+c)}
}


func main(){
    x,y := getm("you're", "learning", "golang")
    fmt.Printf("%v \n %v \n", x,y)
    fmt.Printf("%v \n %v \n",x[0],y[2])
}

Slices in Go

99 times out of 100 you will use a slice instead of an array when working with ordered lists.

Arrays are fixed in size. Once you make an array like [10]int you can't add an 11th element.

A slice is a dynamically-sized, flexible view of the elements of an array.

The zero value of slice is nil.

Non-nil slices always have an underlying array, though it isn't always specified explicitly.

Remember arrays are created by mentioned size like [3]int, [5]string etc..

But slices will be created without mentioning any sizes, just [].

var a []int{}
b := []int{2, 3, 5, 7, 11, 13}            // or to declare an initialized literal

Slicing an array:

To explicitly create a slice on top of an array we can do:

primes := [6]int{2, 3, 5, 7, 11, 13}
mySlice := primes[1:4]
// mySlice = {3, 5, 7}

The syntax is:

arrayname[lowIndex:highIndex]
arrayname[lowIndex:]
arrayname[:highIndex]
arrayname[:]

Where lowIndex is inclusive and highIndex is exclusive.

lowIndex, highIndex, or both can be omitted to use the entire array on that side of the colon.

Another Example:

package main

import (
    "errors"
    "fmt"
)

const (
    planFree = "free"
    planPro  = "pro"
)

func getMessageWithRetriesForPlan(plan string, messages []string) ([]string, error) {
    switch plan {
        case planPro :
            return messages, nil
        case planFree :
            return messages[0:2], nil
        default :
            return nil, errors.New("Unsupported Plan")
    }
}

func main(){

    a,b := getMessageWithRetriesForPlan("free", []string{"this", "is", "hashnode"})
    if b != nil{
        fmt.Println("Error: ",b)
        return
    }
    fmt.Println(a)
}

Slices Review

Slices wrap arrays to give a more general, powerful, and convenient interface to sequences of data. Except for items with explicit dimensions such as transformation matrices, most array programming in Go is done with slices rather than simple arrays.

Slices hold references to an underlying array, and if you assign one slice to another, both refer to the same array. If a function takes a slice argument, any changes it makes to the elements of the slice will be visible to the caller, analogous to passing a pointer (we'll cover pointers later) to the underlying array. A Read function can therefore accept a slice argument rather than a pointer and a count; the length within the slice sets an upper limit of how much data to read. Here is the signature of the Read() method of the File type in package os

func (f *File) Read(buf []byte) (n int, err error)

Make

Most of the time we don't need to think about the underlying array of a slice. We can create a new slice using the make function:

// func make([]T, len, cap) []T
mySlice := make([]int, 5, 10)

// the capacity argument is usually omitted and defaults to the length
mySlice := make([]int, 5)

Slices created with make will be filled with the zero value of the type.

If we want to create a slice with a specific set of values, we can use a slice literal:

mySlice := []string{"I", "love", "go"}

Notice the square brackets do not have a 3 in them. If they did, you'd have an array instead of a slice.

Capacity

The capacity of a slice is the number of elements in the underlying array, counting from the first element in the slice. It is accessed using the built-in cap() function:

mySlice := []string{"I", "love", "go"}
fmt.Println(cap(mySlice)) // 3

Generally speaking, unless you're hyper-optimizing the memory usage of your program, you don't need to worry about the capacity of a slice because it will automatically grow as needed.

Length

The length of a slice is simply the number of elements it contains. It is accessed using the built-in len() function:

mySlice := []string{"I", "love", "go"}
fmt.Println(len(mySlice)) // 3

Indexing

A programming concept you should already be familiar with is array indexing. You can access or assign a single element of an array or slice by using its index.

mySlice := []string{"I", "love", "go"}
fmt.Println(mySlice[2]) // go

mySlice[0] = "you"
fmt.Println(mySlice) // [you love go]

Another Example :

We send a lot of text messages at Textio, and our API is getting slow and unresponsive.

If we know the rough size of a slice before we fill it up, we can make our program faster by creating the slice with that size ahead of time so that the Go runtime doesn't need to continuously allocate new underlying arrays of larger and larger sizes. By setting the length, the slice can still be resized later, but it means we can avoid all the expensive resizing since we know what we'll need.

Complete the getMessageCosts() function. It takes a slice of messages and returns a slice of message costs.

  1. Pre allocate a slice for the message costs of the same length as the messages slice.

  2. Fill the costs slice with costs for each message. The cost in the cost slice should correspond to the message in the messages slice at the same index. The cost of a message is the length of the message multiplied by 0.01.

package main
func getMessageCosts(messages []string) []float64 {
    n := len(messages)
    mySlice := make([]float64, n)
    for i:=0; i<n; i++ {
        mySlice[i] = float64(len(messages[i])) * 0.1
    }
    return mySlice
}

Variadic

Many functions, especially those in the standard library, can take an arbitrary number of final arguments. This is accomplished by using the "..." syntax in the function signature.

A variadic function receives the variadic arguments as a slice.

func concat(strs ...string) string {
    final := ""
    // strs is just a slice of strings
    for i := 0; i < len(strs); i++ {
        final += strs[i]
    }
    return final
}

func main() {
    final := concat("Hello ", "there ", "friend!")
    fmt.Println(final)
    // Output: Hello there friend!
}

The familiar fmt.Println() and fmt.Sprintf() are variadic! fmt.Println() prints each element with space delimiters and a newline at the end.

func Println(a ...interface{}) (n int, err error)

Spread Operator :

Variadic syntax is useful when u want to allow user to pass as many arguments as he can.

Spread operator is used to pass a single slice to a variadic function, instead of manually passing all dynamic inputs 1 after other in calling.

The spread operator consists of three dots following the slice in the function call.

func printStrings(strings ...string) {
    for i := 0; i < len(strings); i++ {
        fmt.Println(strings[i])
    }
}

func main() {
    names := []string{"bob", "sue", "alice"}
    printStrings(names...)
}

Generally to make ur functions variadic, u take slice as input with … syntax in function definition.

So that u pass dynamic values at calling, it converts to a single slice and function uses it.

Spread operator is inverse to this.

With slice operator u are passing a single slice as argument at calling, but 1 slice will be broken into dynamic values for a variadic function, and those dynamic values are passed as input to the variadic function.

Append

The built-in append function is used to dynamically add elements to a slice:

func append(slice []Type, elems ...Type) []Type

If the underlying array is not large enough, append() will create a new underlying array and point the returned slice to it.

Notice that append() is variadic, the following are all valid:

slice = append(slice, oneThing)
slice = append(slice, firstThing, secondThing)
slice = append(slice, anotherSlice...)

Slice of Slices

Slices can hold other slices, effectively creating a matrix, or a 2D slice.

rows := [][]int{}

Another example :

We support various visualization dashboards on Textio that display message analytics to our users. The UI for our graphs and charts is built on top of a grid system. Let's build some grid logic.

Complete the createMatrix function. It takes a number of rows and columns and returns a 2D slice of integers. The value of each cell is i * j where i and j are the indexes of the row and column respectively. Basically, we're building a multiplication chart.

For example, a 5x10 matrix, produced from calling createMatrix(5, 10), would look like this:

[0  0  0  0  0  0  0  0  0  0]
[0  1  2  3  4  5  6  7  8  9]
[0  2  4  6  8 10 12 14 16 18]
[0  3  6  9 12 15 18 21 24 27]
[0  4  8 12 16 20 24 28 32 36]

Code :

package main

func createMatrix(rows, cols int) [][]int {
    matrix := [][]int{}
    for i:=0; i<rows; i++ {
        row := []int
        for j:=0; j<cols; j++ {
            row = append(row, i*j)
        }
        matrix = append(matrix, row)
    }
    return matrix
}

Range

Go provides syntactic sugar to iterate easily over elements of a slice:

for INDEX, ELEMENT := range SLICE {
}

The element is a copy of the value at that index.

For example:

fruits := []string{"apple", "banana", "grape"}
for i, fruit := range fruits {
    fmt.Println(i, fruit)
}
// 0 apple
// 1 banana
// 2 grape

Tricky Slices

The append() function changes the underlying array of its parameter AND returns a new slice. This means that using append() on anything other than itself is usually a BAD idea.

// dont do this!
someSlice = append(otherSlice, element)

Take a look at these head-scratchers:

Example 1: Works as Expected

a := make([]int, 3)
fmt.Println("len of a:", len(a))
fmt.Println("cap of a:", cap(a))
// len of a: 3
// cap of a: 3

b := append(a, 4)
fmt.Println("appending 4 to b from a")
fmt.Println("b:", b)
fmt.Println("addr of b:", &b[0])
// appending 4 to b from a
// b: [0 0 0 4]
// addr of b: 0x44a0c0

c := append(a, 5)
fmt.Println("appending 5 to c from a")
fmt.Println("addr of c:", &c[0])
fmt.Println("a:", a)
fmt.Println("b:", b)
fmt.Println("c:", c)
// appending 5 to c from a
// addr of c: 0x44a180
// a: [0 0 0]
// b: [0 0 0 4]
// c: [0 0 0 5]

With slices a, b, and c, 4 and 5 seem to be appended as we would expect. We can even check the memory addresses and confirm that b and c point to different underlying arrays.

Example 2: Something Fishy

i := make([]int, 3, 8)
fmt.Println("len of i:", len(i))
fmt.Println("cap of i:", cap(i))
// len of i: 3
// cap of i: 8

j := append(i, 4)
fmt.Println("appending 4 to j from i")
fmt.Println("j:", j)
fmt.Println("addr of j:", &j[0])
// appending 4 to j from i
// j: [0 0 0 4]
// addr of j: 0x454000

g := append(i, 5)
fmt.Println("appending 5 to g from i")
fmt.Println("addr of g:", &g[0])
fmt.Println("i:", i)
fmt.Println("j:", j)
fmt.Println("g:", g)
// appending 5 to g from i
// addr of g: 0x454000
// i: [0 0 0]
// j: [0 0 0 5]
// g: [0 0 0 5]

In this example, however, when 5 is appended to g it overwrites j's fourth index because j and g point to the same underlying array. The append() function only creates a new array when there isn't any capacity left. We created i with a length of 3 and a capacity of 8, which means we can append 5 items before a new array is automatically allocated.

Again, to avoid bugs like this, you should always use the append function on the same slice the result is assigned to:

mySlice := []int{1, 2, 3}
mySlice = append(mySlice, 4)

Maps in Go :

Maps are similar to JavaScript objects, Python dictionaries, and Ruby hashes. Maps are a data structure that provides key->value mapping.

The zero value of a map is nil.

We can create a map by using a literal or by using the make() function:

ages := make(map[string]int)
ages["John"] = 37
ages["Mary"] = 24
ages["Mary"] = 21 // overwrites 24
ages = map[string]int{
  "John": 37,
  "Mary": 21,
}
// Alternatively, without make()

The len() function works on a map, it returns the total number of key/value pairs.

ages = map[string]int{
  "John": 37,
  "Mary": 21,
}
fmt.Println(len(ages)) // 2

Another example :

package main
import "errors"

type user struct {
    name        string
    phoneNumber int
}


func getUserMap(names []string, phoneNumbers []int) (map[string]user, error) {
    if len(names) != len(phoneNumbers) {
        return make(map[string]user), errors.New("Invalid sizes")
    }
    result := make(map[string]user)
    n := len(name)
    for i:=0; i<n; i++ {
        result[names[i]] = user{ name: names[i], phoneNumber: phoneNumbers[i] }
    }
    return result, nil
}

Mutations :

Insert an Element :

m[key] = elem

Get an Element :

elem = m[key]

Delete an Element :

delete(m, key)

Check if a key exists :

elem, ok := m[key]
  • If key is in m, then ok is true and elem is the value as expected.

  • If key is not in the map, then ok is false and elem is the zero value for the map's element type.

Another Example :

/*
Complete the deleteIfNecessary function.
 The user struct has a scheduledForDeletion field that determines if they are scheduled for 
deletion or not.

If the user doesn't exist in the map, return the error not found.
If they exist but aren't scheduled for deletion, return deleted as false with no errors.
If they exist and are scheduled for deletion, return deleted as true 
with no errors and delete their record from the map.
*/

package main
import "errors"
type user struct {
    name                 string
    number               int
    scheduledForDeletion bool
}

func deleteIfNecessary(users map[string]user, name string) (deleted bool, err error) {
    usr, ok := users[name]
    if !ok {
        return false, errors.New("not found")
    }
    if !usr.scheduledForDeletion {
        return false, nil
    }
    delete(users, name)
    return true, nil
}

Like slices, maps are also passed by reference into functions. This means that when a map is passed into a function we write, we can make changes to the original — we don't have a copy.

Key Types

Any type can be used as the value in a map, but keys are more restrictive.

Read the following section of the official Go blog:

As mentioned earlier, map keys may be of any type that is comparable. The language spec defines this precisely, but in short, comparable types are boolean, numeric, string, pointer, channel, and interface types, and structs or arrays that contain only those types. Notably absent from the list are slices, maps, and functions; these types cannot be compared using ==, and may not be used as map keys.

It's obvious that strings, int, and other basic types should be available as map keys, but perhaps unexpected are struct keys. Struct can be used to key data by multiple dimensions. For example, this map of maps could be used to tally web page hits by country:

hits := make(map[string]map[string]int)

This is a map of string to (map of string to int). Each key of the outer map is the path to a web page with its own inner map. Each inner map key is a two-letter country code. This expression retrieves the number of times an Australian has loaded the documentation page:

n := hits["/doc/"]["au"]

Unfortunately, this approach becomes unwieldy when adding data, as for any given outer key you must check if the inner map exists, and create it if needed:

func add(m map[string]map[string]int, path, country string) {
    mm, ok := m[path]
    if !ok {
        mm = make(map[string]int)
        m[path] = mm
    }
    mm[country]++
}
add(hits, "/doc/", "au")

On the other hand, a design that uses a single map with a struct key does away with all that complexity:

type Key struct {
    Path, Country string
}
hits := make(map[Key]int)

When a Vietnamese person visits the home page, incrementing (and possibly creating) the appropriate counter is a one-liner:

hits[Key{"/", "vn"}]++

And it’s similarly straightforward to see how many Swiss people have read the spec:

n := hits[Key{"/ref/spec", "ch"}]

Count Instances

Remember that you can check if a key is already present in a map by using the second return value from the index operation.

You can combine an if statement with an assignment operation to use the variables inside the if block:

names := map[string]int{}
missingNames := []string{}

if _, ok := names["Denna"]; !ok {
    // if the key doesn't exist yet,
    // append the name to the missingNames slice
    missingNames = append(missingNames, "Denna")
}

Another Example :

/*
Each time a user is sent a message, their username is logged in a slice. 
We want a more efficient way to count how many messages each user received.

Implement the updateCounts function. It takes as input:

messagedUsers: a slice of strings.
validUsers: a map of string -> int.

It should update the validUsers map with the number of times each user has received a message.
Each string in the slice is a username, but they may not be valid. 
Only update the message count of valid users.

So, if "benji" is in the map and appears in the slice 3 times, 
the key "benji" in the map should have the value 3.
*/

package main

func updateCounts(messagedUsers []string, validUsers map[string]int) {
    for _, user := range(messagedUsers) {
        if _, ok := validUsers[user]; ok {
            validUsers[user]++;
        }
    }
}

Effective Go

Like slices, maps hold references to an underlying data structure. If you pass a map to a function that changes the contents of the map, the changes will be visible in the caller.

Map Literals :

Maps can be constructed using the usual composite literal syntax with colon-separated key-value pairs, so it's easy to build them during initialization.

var timeZone = map[string]int{
    "UTC":  0*60*60,
    "EST": -5*60*60,
    "CST": -6*60*60,
    "MST": -7*60*60,
    "PST": -8*60*60,
}

Missing Keys :

An attempt to fetch a map value with a key that is not present in the map will return the zero value for the type of the entries in the map. For instance, if the map contains integers, looking up a non-existent key will return 0. A set can be implemented as a map with value type bool. Set the map entry to true to put the value in the set, and then test it by simple indexing.

attended := map[string]bool{
    "Ann": true,
    "Joe": true,
    ...
}

if attended[person] { // will be false if person is not in the map
    fmt.Println(person, "was at the meeting")
}

Sometimes you need to distinguish a missing entry from a zero value. Is there an entry for "UTC" or is that 0 because it's not in the map at all? You can discriminate with a form of multiple assignment.

var seconds int
var ok bool
seconds, ok = timeZone[tz]

For obvious reasons, this is called the “comma ok” idiom. In this example, if tz is present, seconds will be set appropriately and ok will be true; if not, seconds will be set to zero and ok will be false. Here's a function that puts it together with a nice error report:

func offset(tz string) int {
    if seconds, ok := timeZone[tz]; ok {
        return seconds
    }
    log.Println("unknown time zone:", tz)
    return 0
}

Now we can redo the first example of a set by using a more efficient map with empty structs, which don't take up any space in memory:

attended := map[string]struct{}{
    "Ann": {},
    "Joe": {},
    ...
}

if _, ok := attended[person]; ok {
    fmt.Println(person, "was at the meeting")
}

To delete a map entry, use the built-in delete function, whose arguments are the map and the key to be deleted. It's safe to do this even if the key is already absent from the map.

delete(timeZone, "PDT")  // Now on Standard Time

Nested

Maps can contain maps, creating a nested structure. For example:

map[string]map[string]int
map[rune]map[string]int
map[int]map[string]map[string]int

Another Example :

package main

func getNameCounts(names []string) map[rune]map[string]int {
    Names := make(map[rune]map[string]int)
    for _, nme := range(names) {
        name := []rune(nme)
        if _, ok := Names[name[0]]; !ok {
            Names[name] = make(map[string]int)
        }
        if _, ok := Names[name[0]][name]; !ok {
            Names[name[0]][name] = 1
        }
        Names[name[0]][name]++;
    }
    return Names;
}

/*
Complete the getNameCounts function. 
It takes a slice of strings names and returns a nested map. 
The parent map's keys are all the unique first characters (see runes) of the names, 
the nested maps keys are all the names themselves, and the value is the count of each name.
*/

/* 
EXAMPLE OUTPUT :
b: {
    billy: 2,
    bob: 1
},
j: {
    joe: 1
}
*/
0
Subscribe to my newsletter

Read articles from Rajesh Gurajala directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Rajesh Gurajala
Rajesh Gurajala