Johnny.sh

Arrays, Slices, Maps, Structs

Go has a few different composite types, similar to “collections” in Java, but a lot simpler. Anything iterable, a collection is a composite type.

Arrays

Arrays in Go have a fixed length. You can’t add or remove items from an array in Go. Because of this, they are not used very often.

They look like this:

var a [3]int

This declares an array of integers with a length of 3. When we declare an array this way, without setting the items, each item will be initialized to 0. An array always will have the number of items you initialized it to have; if not set explicitly the items will be initialized to be the zero value of the type of that array.

You can also explicitly set the value of an index when initializing an array, like this:

r := [...]int{99: 1}

This creates an array with 100 items, all with value 0 except for the last item. Initializing an array with ... lets the size be determined automatically by the compiler.

Slices

Slices behave for the most part like arrays, but the length is variable. The main difference is that a slice declaration just uses the [] directly, no length or anything in the brackets. We use slices a lot more commonly in Go.

Like arrays, we can access and get items by index.

Unlike arrays, we can initialize slices using the built-in function make:

s := make([]string, 3)
fmt.Println("emp:", s)
s[0] = "yoooo"
fmt.Println("length", len(s))
/*
emp: [  ]
length 3
*/

There are a few other differences with arrays, such as:

  • We can use the built-in append function to add items
  • We can use the slice operator syntax to return a subset of a slice, which is like this [low:high]
  • Arrays of comparable types (numbers, strings, etc.) can be compared directly using the == operator. We can’t do this with slices, slices are passed by reference identity, not value. So we’d have to use a “deep equal” function of some kind to compare slices.

Maps

In Go, the map is an implementation of a traditional hash table or hash map. It’s an unordered collection of key/value pairs. A map type is written like map[K]V, in which K is the type of the key, and V is the type of the value. The type you choose for the key K must be comparable using == for the map to work properly.

Similarly to slices, we can initialize a map with the make built-in function, or we can initialize using map literal syntax.

ages := make(map[string]int)
leaderAges := map[string]int{
  "john": 32,
  "charlie": 34,
}

Note the trailing comma. There’s a built-in function called delete which you can use to remove entries from the map.

delete(leaderAges, "john")

Notably, unlike JavaScript, maps are safe to work with if the value you seek is empty. When you try to access a value in a map which hasn’t been added, it will be “added” but just initialized to the zero value of whatever type was specified for the values of the map (V).

fmt.Println(leaderAges["bob"]) // 0, even tho hasn't been added
leaderAges["bob"]++
fmt.Println(leaderAges["bob"]) // 1

This can actually lead to some confusion, how do we know if a map contains a value for a given key or not? The second return value when calling the key accessor allows us to do this.

	dogs := []string{"gruff", "snuff", "duff", "puff", "grover"}
	dogAppearanceCounts := map[string]bool{
		"gruff": true,
	}

	for _, dog := range dogs {
		_, ok := dogAppearanceCounts[dog]
		if !ok {
			dogAppearanceCounts[dog] = true
		}
	}

The word ok is typically used for this.

Map keys must be comparable using ==. You can use arrays as a key, but not, for example, a map as a key for a map. You could conceptually, but you have to use workarounds, such as casting the map to a string then using that as the key.

Structs

Structs are Go’s offering for, basically, objects. Object-oriented stuff is done using structs.

A struct is a structured collection of typed fields.

Capitalized fields in a struct are exported, and available outside the current package.

A struct is not a type interface. However, you can (and must) define a type alias when using structs. This is referred to as a struct type. The instance itself, however, is an struct value.

// struct type declaration
type Location struct {
	Lat, Lon float64
}
// struct value
here := Location{1.400000, 32.121100}

Note that the order fields appear in the struct type declaration is important. In the above example Location, Lat and Lon appear next to eachother, this signifies that the two fields are related. We can also initialize here without specifying Lat and Lon explicitly, instead we can just provide two float64s.

This struct would be considered a different type if declared with Lat and Lon on two lines instead of one.

type Location struct {
	Lat, Lon float64
}

type Dog struct {
	name string
	age  int
	loc  *Location
}

here := Location{1.400000, 32.121100}
fido := Dog{name: "Fido", age: 400, loc: &here}

When nesting structs, we use a pointer. Especially if we are nesting the same type as our current struct.

Struct types can also be declared inline, this is referred to as a struct type literal.

type Point struct{ X, Y int }
p := Point{1, 2}

Also, we can do some “inheritance” type stuff with structs by embedding structs into eachother when we define them.

package main

import "fmt"

type Location struct {
	Lat, Lon float64
}

type Animal struct {
	alive bool
	legs  int
	age   int
}

type Domesticated struct {
	Animal
	name  string
	owner *Animal
}

type Dog struct {
	Domesticated
	loc *Location
}

func main() {
	here := Location{1.400000, 32.121100}
	fido := Dog{loc: &here}
	fido.owner = &Animal{legs: 2}
	fido.name = "fido"
	fmt.Println(fido)
}

But, be careful: type embedding is not inheritance. Major footgun alert.

Struct Methods

One last note about structs: you can define methods on struct, in a kind of object-oriented-programming type style.

type Account struct {
  id  int
  bal float64
}

func (a *Account) String() string {
  return fmt.Sprintf("Account[%d]: $0.2f", a.id, a.bal)
}

In this example, we defined a method called String on the Account struct. Now any instance of Account will have a method called .String() on which you can call, or will be called by things like fmt.

The a *Account is what does this. This is called the “receiver”. In this case it’s a pointer receiver. Instead of using something like this or self like Ruby or OOP languages, we get a reference to the parent/enclosing object in the receiver definition. We can then reference the receiver inside our method body (a.id etc.).

Note: this is a pointer. You can also define a non-pointer receiver, but you can’t edit it inside the method body.

type Circle struct {
  Radius float64
}

func (c Circle) Area() float64 {
  return 3.14 * c.Radius * c.Radius
}

It essentially a “read only” receiver, immutable.

Last modified: February 27, 2023
/about
/uses
/notes
/talks
/projects
/podcasts
/reading-list
/spotify
© 2024