Slices and Maps in Go

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.
Pre allocate a slice for the message costs of the same length as the
messages
slice.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 inm
, thenok
istrue
andelem
is the value as expected.If
key
is not in the map, thenok
isfalse
andelem
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
}
*/
Subscribe to my newsletter
Read articles from Rajesh Gurajala directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
