Back

3. Composite Types


Arrays

Arrays are hardly ever used in Go, due to them being very rigid - they are Static Arrays.

Creating an Array

All the elements of the array must be of the type that’s specified:

var x [3]int

This creates an array x of 3 ints. Since no values were specified, all of them are initialised to the zero value of an int, which is 0 - [0, 0, 0]

var y = [3]int{10, 20, 30}

This creates an array of 3 specified int values - [10, 20, 30]

var z = [12]int{1, 5:4, 6, 10: 100, 15}

This create a “sparse” array - most of the elements are 0 and we specify the indices we want. This will create [1, 0, 0, 0, 0, 4, 6, 0, 0, 0, 100, 15]

You can also create an array without passing the number in, and it will be inferred instead:

var y = [...]int{10, 20, 30} // same as the y above

Comparisons

And we can use == and != to compare arrays

var x = [...]int{1, 2, 3}
var y = [3]int{1, 2, 3}
x == y // true

Matrices

You can simulate multi-dimensional arrays as well. It doesn’t have true matrix support but this will work by creating an array of length 2 whose type is an array of ints of length 3:

var x [2][3]int

Reading & Writing

Like most languages, you read an array by passing in the index:

var x = [3]int{1, 2, 3}
fmt.Println(x[0]) // 1

x[0] = 10
fm.Println(x[0]) // 10

You can’t write past the end of an array of use a negative index, and an out-of-bounds read or write will compile but will cause a panic which will be discussed later.

Getting the length of an array

You can use the function len to get the length:

len(x)

Slices

Unless you know the exact length of an array ahead of time, you’ll use a slice. For example, some of the cryptographic functions in the standard library return arrays because the size of the checksums are defined as part of the algorithm

The main reason why Arrays exist in GO is to provide the backing store for slices, which are one of the most useful features of Go.

Creating a Slice

We don’t have to declare the length in the type:

var x = []int{10, 20, 30}
var y = []int{1, 5:4, 6, 10:100, 15}
var z = [][]int

Note that this is not […] as that will create an Array x = [10, 20, 30] y = [1, 0, 0, 0, 0, 4, 6, 0, 0, 0, 100, 15] z is a matrix

var x []int

If we don’t pass in any value, x is assigned the zero value for a slice, which is nil - this is slightly different than null seen in other languages as nil is an ********identifier************ that represents the lack of a value for some types.

Like untyped numeric constants, nil has no type so it can be assigned or compared against values of different types. A nil slice contains nothing.

Comparisons

A slice is the first type we’ve seen that IS NOT COMPARABLE.

If we try to use == to see if two slices are comparable, it will throw a compile-time error. The only comparison we can make is with nil

var x []int
fmt.Println(x == nil) // true

Reading, Writing & Length

Just like an array, reading is done with the index & similar to arrays, you can’t read out of bounds or pass in a negative index.

x[10]

and getting the length uses the len() function:

len(x) //  will be 0 for unitialized slices

Append

We can grow slices with the append function:

var x []int
x = append(x, 10)

var y = []int{5, 7, 9}
y = append(y, 10)
y = append(y, 14, 15, 17)

One slice is appended onto another using the ... operator to expand the source slice into individual values.

y := []int{20, 30, 40}
x = append(x, y...)

If we don’t assign a value to the append function, we will get a compile time error.

Capacity

A slice is a sequence of values. Each element in a slice is assigned to consecutive memory locations, which makes it quick to read or write.

How slices grow under the hood

When a slice grows via the append method, it takes time for the Go runtime to allocate new memory and copy the existing data from the old memory to the new, then garbage collect the old. Go will:

Just as the built-in len function returns the current length of a slice, the built-in cap function returns the current capacity of a slice.

var x []int
fmt.Println(x, len(x), cap(x)) // [] 0 0

x = append(x, 1)
fmt.Println(x, len(x), cap(x)) // [1] 1 1

x = append(x, 2)
fmt.Println(x, len(x), cap(x)) // [1, 2] 2 2

x = append(x, 3)
fmt.Println(x, len(x), cap(x)) // [1, 2, 3] 3 4

x = append(x, 4)
fmt.Println(x, len(x), cap(x)) // [1, 2, 3, 4] 4 4

x = append(x, 5)
fmt.Println(x, len(x), cap(x)) // [1, 2, 3, 4, 5] 5 8

Make

While it’s nice that they grow automatically, it’s more efficient if we size them ahead of time if we know that the slice will be fairly large.

x := make([]int, 5) // [0, 0, 0, 0, 0]

This creates an int slice with length of 5 and capacity of 5. Since it has a length of 5, x[0] thorugh x[4] are valid elements and they are all initialised to 0.

One common mistake is to try an populate those values with append:

x := make([]int, 5)
x = append(x, 10)

This 10 is placed at the end of the slice - [0, 0, 0, 0, 0, 10] and we have a length of 6 and capacity of 10 (as this is doubled).

We can also specify an initial capacity:

x := make([]int, 5, 10)
y := make([]int, 0, 10)

This creates an int slice x with a length of 5 and capacity of 10, and an int slice y with zero length and capacity of 10

In the case of y, we have a non-nil slice with length of 0. Since the length is 0, we can’t directly index into it but we can append values directly with:

y = append(y, 1, 2, 3, 4, 5)

Declaring your slice

Slicing Slices

A slice expression create a slice from a slice.

x := []int{1, 2, 3, 4} // [ 1, 2, 3, 4]

y := x[:2] // [1, 2]
z := x[1:] // [2, 3, 4]
d := x[1:3] // [2, 3]
e := x[:] // [1, 2, 3, 4]

It INCLUDES the starting offset and EXCLUDES the ending offset.

Shared Storage

When you slice from a slice, you are not making a copy of the data. Instead you now have 2 variables sharing memory. This means that changes to an element in a slice affect all slices that share that element.

This causes weird issues with append so you should never use append with shallow slices, instead use a full slice:

x := []int{1, 2, 3, 4} // [ 1, 2, 3, 4]

y := x[:2:2]
z := x[2:4:4]

Both y and z have a capacity of 2 because we limited the capacity of the sub-slices to their lengths. This means that appending additional elements onto y and z created new slices that don’t interact with the other slices.

Converting Arrays to Slices

We can slice an array as well. However be aware that taking a slice from an array has the same memory-sharing properties as taking a slice from a slice.

 	x := [4]int{5, 6, 7, 8}
	y := x[:2]
	z := x[2:]
	x[0] = 10
  y[1] = 4
	fmt.Println("x:", x)
	fmt.Println("y:", y)
	fmt.Println("z:", z)

// x: [10 4 7 8] <-- y[1] = 4 modified this
// y: [10 4] <-- x[0] = 10 modified this
// z: [7 8]

Copy

If you want a slice that’s independent from the original, we can use the copy function

  x := []int{1, 2, 3, 4}
	y := make([]int, 4)
	num := copy(y, x)
	fmt.Println(y, num)

// [1 2 3 4] 4

It copies as many values as it can from the source to destination, limited by whichever slice is smaller, and returns the number of elements copied. The capacity doesn’t matter, only the length.

x := []int{1, 2, 3, 4}
y := make([]int, 2)
num = copy(y, x)

This will only copy the first two elements. y is set to [1 2] and num is 2

We can also copy from the middle:

x := []int{1, 2, 3, 4]
y := make([]int, 2)
num = copy(y, x[2:])

y is now [3 4]

It also allows you to copy between two slices that cover overlapping sections of an underlying slice

x := []int{1, 2, 3, 4]
num = copy(x[:3], x[1:])

// [2 3 4 4] <- num is 3

This copies the last three values in x on top of the first three values of x

You can use copy with arrays by taking a slice of the array:

  x := []int{1, 2, 3, 4}
	d := [4]int{5, 6, 7, 8}
	y := make([]int, 2)
	copy(y, d[:])
	fmt.Println(y)
	copy(d[:], x)
	fmt.Println(d)

// y = [5 6]
// d = [1 2 3 4]

This copies the first 2 values of d into y, then all the values of x into d


Strings, Runes & Bytes

Under the hood, Go uses bytes to represent a string that don’t have to be in any particular character encoding, however several Go library functions (and the for-range loop) assume that a string is composed of a sequence of UTF-8 encoded code points.

var s string = "Hello there"
var b byte = s[6]

Like arrays & slices, strings are 0-indexed. b is assigned to the 7th character which is “t”

Substrings

The slice expression notation also works with strings. Again, the first is inclusive and the second is exclusive.

var s string = "Hello there"
var s2 string = s[4:7] // inclusive of 4, excludes the 7th
var s3 string = s[:5] // up to but excluding the 5th
var s4 string = s[6:] // the 6th onwards

s2 = “o t”, s3 = “Hello”, s4 = “there”

Usually, with simple UTF-8 strings, each character will be one byte. However, in certain cases there could be characters that take up multiple bytes, e.g. emojis: 🌞.

var s string = "Hello 🌞"
var s2 string = s[4:7]
fmt.Println(len(s)) // returns 10, not 7 because the Sun emoji takes up 4 bytes

Instead of getting “o 🌞”, we get “o �” because it only copied the first byte of the emoji’s code point which is invalid.

Due to the complicated relationship between runes, strings & bytes, Go has some interesting type conversions:

var a rune = 'x' // 120
var s string = string(a) // x
var b byte = 'y' // 121
var s2 string = string(b) // y

A common bug for new Go developers is converting an int into a string:

var x int = 65
var y = string(x) // "A" not "65".
var s string = "Hello, 🪿"
	var bs []byte = []byte(s)
	var rs []rune = []rune(s)

	fmt.Println(s) // Hello, 🪿
	fmt.Println(bs) // [72 101 108 108 111 44 32 240 159 170 191]
	fmt.Println(rs) // [72 101 108 108 111 44 32 129727]

Rather than using the slice and index method, you should extract sub-strings and code points from strings using the functions in the strings and unicode/utf8 packages in the standard library.


Maps

The map type is written as map[keyType]valueType.

Declaring a map

The Nil map

var nilMap map[string]int

This nilMap is declared to be a map with string keys and int values. The zero value for a map is nil, which has a length of 0. If you try to real a nil map, you’ll get the zero value for the map’s value type. However, attempting to write to a nil map variable causes a panic.

An empty map literal

totalWins := map[string]int{}

In this case, it has a length of 0 but you can read and write to a map assigned an empty map literal.

teams := map[string][]string {
  "Orcas": []string{"Fred", "Ralph", "Bijoue"},
  "Lions": []string{"Sarah", "Peter"},
  "Kittens": []string{"Waldo", "Raul"}
}

A nonempty map literal.

Make

If you know how many key-value pairs you intend to put in the map, you can use make to cretea a map with a default size:

ages := make(map[int][]string, 10)

This will still have a length of 0, and they can grow past the initially specified size

Similarities with Slices

Reading & Writing

  totalWins := map[string]int{}
	totalWins["Orcas"] = 1
	totalWins["Lions"] = 2
	fmt.Println(totalWins["Orcas"])
	fmt.Println(totalWins["Kittens"])
	totalWins["Kittens"]++
	fmt.Println(totalWins["Kittens"])
	totalWins["Lions"] = 3
	fmt.Println(totalWins["Lions"])

Outputs 1, 0, 1, 3

The totalWins of Kittens defaults to the 0 value of an int, which is 0. You can also use the ++ operator to increment a numeric value for a map key, which is faster to run as well as type than totalWins["Kittens"]= totalWins["Kittens"] + 1.

Checking for existence of key

We sometimes need to check if a key exists in a map at all, rather than just get the zero value of it given the key.

m := map[string]int{
	"hello": 5,
	"world": 0,
}
v, ok := m["hello"]
fmt.Println(v, ok) // 5 true

v, ok = m["world"]
fmt.Println(v, ok) // 0 true

v, ok = m["goodbye"]
fmt.Println(v, ok) // 0 false

“ok” is a boolean that shows us whether the key exists in the map

Deleting from Maps

There is a built-in delete function:

m := map[string]int{
	"hello": 5,
	"world": 0,
}
delete(m, "hello")

If the key isn’t present or the entire map is nil, nothing happens. It doesn’t return a value.

Using Maps as Sets

Many languages include a Set in their standard library. This is a data type that ensures that there is at most one of a value, but doesn’t guarantee that the values are in any particular order.

Go doesn’t provide one out of the box, but we can create one with a map very easily.

  intSet := map[int]bool{}
	vals := []int{5, 10, 2, 5, 8, 7, 3, 9, 1, 2, 10}
	for _, v := range vals {
		intSet[v] = true
	}
	fmt.Println(len(vals), len(intSet))
	fmt.Println(intSet[5])
	fmt.Println(intSet[500])
	if intSet[100] {
		fmt.Println("100 is in the set")
	}

We want a set of ints so we create a map where the keys are of int type and the values are of **bool** type. We iterate over the values in *vals***** using a *****for-range***** loop & place them in the ***intSet.***

Summary

MethodCode
Constructm := map[key]value
Insertm[k] = v
Lookupv = m[k]
Deletedelete(m, k)
Iteratefor k, v := range m
Sizelen(m)

Operations run in O(1) constant time.

Structs

Maps are convenient but they don’t define an API since there’s no way to constrain a map to only allow certain keys. Also, all of the values in a map must be of the same type.

Creating a Struct

type person struct {
	name string
	age int
	pet string
}

var fred person
bob := person{}

A struct is defined with the keyword type , the name of the struct type and the keyword struct. Unlike a map, these are not comma-separated.

We can either use the struct literal format, where every field in the struct must be specified and the order matters.

julia := person{
	"Julia",
	40,
	"cat",
}

Or you can define it like a map:

beth := person{
	age: 30,
	name: "Beth",
}

pet is set to the zero value of string, which is “”

Reading & Writing to a struct

We can use the dot notation to read and write to a struct.

bob.name = "Bob"
fmt.Println(beth.name)

Anonymous Structs

You can declare that a variable implements a struct type without first giving the struct type a name.

var person struct {
	name string
	age int
	pet string
}

person.name = "bob"
person.age = 50
person.pet = "dog"

pet := struct {
	name string
	king string
}{
	name: "Fido",
	kind: "dog",
}

The types of the variables person and pet are anonymous structs. You assign and read fields just like a named struct type, and you initialise an instance of it in the same way.

Why would you ever use this?

  1. Translating external data into a struct & Translating a struct into external data (like JSON or protocol buffers)

This is called unmarshaling and marshaling data which will be covered later when we learn encoding/json

  1. Writing tests - we’ll use a slice of anonymous structs when writing table-driven tests in chapter 13

Comparing and Converting Structs

Whether or not a struct is comparable depends on the struct’s fields. If the structs are composed of comparable types are comparable and those with slice or map fields are not.

However, the usual == and != will not work. Given the following struct:

type firstPerson struct {
	name string
	age int
}

We can use a type conversion to convert an isntance of firstPerson to secondPerson, but we can’t compare firstPerson with thirdPerson due to the different order & we can’t compare with fourthPerson due to different naming. We can’t compare with the fifthPerson due to the extra field.

type secondPerson struct {
	name string
	age int
}

type thirdPerson struct {
	age int
	name string
}

type fourthPerson struct {
	firstName string
	age int
}

type fifthPerson struct {
	name string
	age int
	favoriteColor string
}

However, with anonymous structs, you can compare them without a type conversion (assuming both structs have the same fields).

PrevNext