Book Image

Go for DevOps

By : John Doak, David Justice
5 (1)
Book Image

Go for DevOps

5 (1)
By: John Doak, David Justice

Overview of this book

Go is the go-to language for DevOps libraries and services, and without it, achieving fast and safe automation is a challenge. With the help of Go for DevOps, you'll learn how to deliver services with ease and safety, becoming a better DevOps engineer in the process. Some of the key things this book will teach you are how to write Go software to automate configuration management, update remote machines, author custom automation in GitHub Actions, and interact with Kubernetes. As you advance through the chapters, you'll explore how to automate the cloud using software development kits (SDKs), extend HashiCorp's Terraform and Packer using Go, develop your own DevOps services with gRPC and REST, design system agents, and build robust workflow systems. By the end of this Go for DevOps book, you'll understand how to apply development principles to automate operations and provide operational insights using Go, which will allow you to react quickly to resolve system failures before your customers realize something has gone wrong.
Table of Contents (21 chapters)
1
Section 1: Getting Up and Running with Go
10
Section 2: Instrumenting, Observing, and Responding
14
Section 3: Cloud ready Go

Using arrays and slices

Languages require more than the basic types to hold data. The array type is one of the core building blocks in lower-level languages, providing the base sequential data type. For most day-to-day use, Go's slice type provides a flexible array that can grow as data needs grow and can be sliced into sections in order to share views of the data.

In this section, we will talk about arrays as the building blocks of slices, the difference between the two, and how to utilize them in your code.

Arrays

The base sequential type in Go is the array (important to know, but rarely used). Arrays are statically sized (if you create one that holds 10 int types, it will always hold exactly 10 int types).

Go provides an array type designated by putting [size] before the type you wish to create an array of. For example, var x [5]int or x := [5]int{} creates an array holding five integers, indexed from 0 to 4.

An assignment into an array is as easy as choosing the index. x[0] = 3 assigns 3 to index 0. Retrieving that value is as simple as referring to the index; fmt.Println(x[0] + 2) will output 5.

Arrays, unlike slices, are not pointer wrapper types. Passing an array as a function argument passes a copy:

func changeValueAtZeroIndex(array [2]int) {
     array[0] = 3
     fmt.Println("inside: ", array[0]) // Will print 3
}
func main() {
     x := [2]int{}
     changeValueAtZeroIndex(x)
     fmt.Println(x) // Will print 0
}

Arrays present the following two problems in Go:

  • Arrays are typed by size – [2]int is distinct from [3]int. You cannot use [3]int where [2]int is required.
  • Arrays are a set size. If you need more room, you must make a new array.

While it is important to know what arrays are, the most common sequential type used in Go is the slice.

Slices

The easiest way to understand a slice is to see it as a type that is built on top of arrays. A slice is a view into an array. Changing what you can see in your slice's view changes the underlying array's value. The most basic use of slices acts like arrays, with two exceptions:

  • A slice is not statically sized.
  • A slice can grow to accommodate new values.

A slice tracks its array, and when it needs more room, it will create a new array that can accommodate the new values and copies the values from the current array into the new array. This happens invisibly to the user.

Creating a slice can be done similarly to an array, var x = []int or x := []int{} . This creates a slice of integers with a length of 0 (which has no room to store values). You can retrieve the size of the slice using len(x).

We can create a slice with initial values easily: x := []int{8,4,5,6}. Now, we have len(x) == 4, indexed from 0 to 3.

Similar to arrays, we can change a value at an index by simply referencing the index. x[2] = 12 will change the preceding slice to []int{8,4,12,6}.

Unlike arrays, we can add a new value to the slice using the append command. x = append(x, 2) will cause the underlying x array references to be copied to a new array and assigns the new view of the array back to x. The new value is []int{8,4,12,6,2}. You may append multiple values by just putting more comma-delimited values in append (that is, x = append(x, 2, 3, 4, 5)).

Remember that slices are simply views into a trackable array. We can create new limited views of the array. y := x[1:3] creates a view (y) of the array, yielding []int{4, 12} (1 is inclusive and 3 is exclusive in [1:3]). Changing the value at y[0] will change x[1]. Appending a single value to y via y = append(y, 10)will change x[3], yielding []int{8,4,12,10,2}.

This kind of use isn't common (and is confusing), but the important part is to understand that slices are simply views into an array.

While slices are a pointer-wrapped type (values in a slice passed to a function that are changed will change in the caller as well), a slice's view will not change.

func doAppend(sl []int) {
     sl = append(sl, 100)
     fmt.Println("inside: ", sl) // inside:  [1 2 3 100]
}
func main() { 
     x := []int{1, 2, 3}
     doAppend(x)
     fmt.Println("outside: ", x) // outside:  [1 2 3]
}

In this example, the sl and x variables both use the same underlying array (which has changed in both), but the view for x does not get updated in doAppend(). To update x to see the addition to the slice would require passing a pointer to the slice (pointers are covered in a future chapter) or returning the new slice as seen here:

func doAppend(sl []int) []int {
     return append(sl, 100)
}
func main() {
     x := []int{1, 2, 3}
     x = doAppend(x)
     fmt.Println("outside: ", x) // outside:  [1 2 3 100]
}

Now that you see how to create and add to a slice, let's look at how to extract the values.

Extracting all values

To extract values from a slice, we can use the older C-type for loop or the more common for...range syntax.

The older C style is as follows:

for i := 0; i < len(someSlice); i++{
     fmt.Printf("slice entry %d: %s\n", i, someSlice[i])
}

The more common approach in Go uses range:

for index, val := range someSlice {
     fmt.Printf("slice entry %d: %s\n", index, val)
}

With range, we often want to use only the value, but not the index. In Go, you must use variables that are declared in a function, or the compiler will complain with the following:

index declared but not used

To only extract the values, we can use _, (which tells the compiler not to store the output), as follows:

for _, val := range someSlice {
     fmt.Printf("slice entry: %s\n", val)
}

On very rare occasions, you may want to only print out indexes and not values. This is uncommon because it will simply count from zero to the number of items. However, this can be achieved by simply removing val from the for statement: for index := range someSlice.

In this section, you have discovered what arrays are, how to create them, and how they relate to slices. In addition, you've acquired the skills to create slices, add data to slices, and extract data from slices. Let's move on to learning about maps next.

Understanding maps

Maps are a collection of key-value pairs that a user can use to store some data and retrieve it with a key. In some languages, these are called dictionaries (Python) or hashes (Perl). In contrast to an array/slice, finding an entry in a map requires a single lookup versus iterating over the entire slice comparing values. With a large set of items, this can give you significant time savings.

Declaring a map

There are several ways to declare a map. Let's first look at using make:

var counters = make(map[string]int, 10)

The example just shared creates a map with string keys and stores data that is an int type. 10 signifies that we want to pre-size for 10 entries. The map can grow beyond 10 entries and the 10 can be omitted.

Another way of declaring a map is by using a composite literal:

modelToMake := map[string]string{
     "prius": "toyota",
     "chevelle": "chevy",
}

This creates a map with string keys and stores the string data. We also pre-populate the entry with two key-value entries. You can omit the entries to have an empty map.

Accessing values

You can retrieve a value as follows:

carMake := modelToMake["chevelle"]
fmt.Println(carMake) // Prints "chevy"

This assigns the chevy value to carMake.

But what happens if the key isn't in the map? In that case, we will receive the zero value of the data type:

carMake := modelToMake["outback"]
fmt.Println(carMake)

The preceding code will print an empty string, which is the zero value of the string type that is used as values in our map.

We can also detect if the value is in the map:

if carMake, ok := modelToMake["outback"]; ok {
     fmt.Printf("car model \"outback\" has make %q", carMake)
}else{
     fmt.Printf("car model \"outback\" has an unknown make")
}

Here we assign two values. The first (carMake) is the data stored in the key (or zero value if not set), and the second (ok) is a Boolean that indicates if the key was found.

Adding new values

Adding a new key-value pair or updating a key's value, is done the same way:

modelToMake["outback"] = "subaru"
counters["pageHits"] = 10

Now that we can change a key-value pair, let's look at extracting values from a map.

Extracting all values

To extract values from a map, we can use the for...range syntax that we used for slices. There are a few key differences with maps:

  • Instead of an index, you will get the map's key.
  • Maps have a non-deterministic order.

Non-deterministic order means that iterating over the data will return the same data but not in the same order.

Let's print out all the values in our carMake map:

for key, val := range modelToMake {
     fmt.Printf("car model %q has make %q\n", key, val)
}

This will yield the following, but maybe not in the same order:

car model "prius" has make "toyota"
car model "chevelle" has make "chevy"
car model "outback" has make "subaru"

Note

Similar to a slice, if you don't need the key, you may use _ instead. If you simply want the keys, you can omit the value val variable, such as for key := range modelToMake.

In this section, you have learned about the map type, how to declare them, add values to them, and finally how to extract values from them. Let's dive into learning about pointers.