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

Understanding Go pointers

Pointers are another essential tool for programming languages for efficient memory use. Some readers may have not encountered pointers in their current language, instead having used its cousin, the reference type. In Python, for example, the dict, list, and object types are reference types.

In this section, we will cover what pointers are, how to declare them, and how to use them.

Memory addresses

In an earlier chapter, we talked about variables for storing data of some type. For example, if we want to create a variable called x that stores an int type with a value of 23, we can write var x int = 23.

Under the hood, the memory allocator allocates us space to store the value. The space is referenced by a unique memory address that looks like 0xc000122020. This is similar to how a home address is used; it is the reference to where the data lives.

We can see the memory address where a variable is stored by prepending & to a variable name:

fmt.Println(&x)

This would print 0xc000122020, the memory address of where x is stored.

This leads to an important concept: functions always make a copy of the arguments passed.

Function arguments are copies

When we call a function and pass a variable as a function argument, inside the function you get a copy of that variable. This is important because when you change the variable, you are only affecting the copy inside the function.

func changeValue(word string) {
     word += "world" 
}

In this code, word is a copy of the value that was passed. word will stop existing at the end of this function call.

func main() {
     say := "hello"
     changeValue(say)
     fmt.Println(say)
}

This prints "hello". Passing the string and changing it in the function doesn't work, because inside the function we are working with a copy. Think of every function call as making a copy of the variable with a copy machine. Editing the copy that came out of the copy machine does not affect the original.

Pointers to the rescue

Pointers in Go are types that store the address of a value, not the value. So, instead of storing 23, it would store 0xc000122020, which is where in memory 23 is stored.

A pointer type can be declared by prepending the type name with *. If we want to create an intPtr variable that stores a pointer to int, we can do the following:

var intPtr *int

You cannot store int in intPtr; you can only store the address of int. To get the address of an existing int, you can use the & symbol on a variable representing int.

Let's assign intPtr the address of our x variable from previously:

intPtr = &x
intPtr now stores 0xc000122020. 

Now for the big question, how is this useful? This lets us refer to a value in memory and change that value. We do that through what is called dereferencing the pointer. This is done with the * operator on the variable.

We can view or change the value held at x by dereferencing the pointer. The following is an example:

fmt.Println(x)             // Will print 23 
fmt.Println(*intPtr)       // Will print 23, the value at x 
*intPtr = 80               // Changes the value at x to 80 
fmt.Println(x)             // Will print 80 

This also works across functions. Let's alter changeValue() to work with pointers:

func changeValue(word *string) {
     // Add "world" to the string pointed to by 'word'
     *word += "world"
}
func main() {
     say := "hello"
     changeValue(&say) // Pass a pointer
     fmt.Println(say) // Prints "helloworld"
}

Note that operators such as * are called overloaded operators. Their meaning depends on the context in which they are used. When declaring a variable, * indicates a pointer type, var intPtr *int. When used on a variable, * means dereference, fmt.Println(*intPtr). When used between two numbers, it means multiply, y := 10 * 2. It takes time to remember what a symbol means when used in certain contexts.

But, didn't you say every argument is a copy?!

I did indeed. When you pass a pointer to a function, a copy of the pointer is made, but the copy still holds the same memory address. Therefore, it still refers to the same piece of memory. It is a lot like making a copy of a treasure map on the copy machine; the copy still points to the place in the world where you will find the treasure. Some of you are probably thinking, But maps and slices can have their values changed, what gives?

They are a special type called a pointer-wrapped type. A pointer-wrapped type hides internal pointers.

Don't go crazy with pointers

While in our examples we used pointers for basic types, typically pointers are used on long-lived objects or for storage of large data that is expensive to copy. Go's memory model uses the stack/heap model. Stack memory is created for exclusive use by a function/method call. Allocation on the stack is significantly faster than on the heap.

Heap allocation occurs in Go when a reference or pointer cannot be determined to live exclusively within a function's call stack. This is determined by the compiler doing escape analysis.

Generally, it is much cheaper to pass copies into a function via an argument and another copy in the return value than it is to use a pointer. Finally, be careful with the number of pointers. Unlike C, it is uncommon in Go to see pointers to pointers, such as **someType, and, in over 10 years of coding Go, I have only once seen a single use for ***someType that was valid. Unlike in the movie Inception, there is no reason to go deeper.

To sum up this section, you have gained an understanding of pointers, how to declare them, how to use them in your code, and where you should probably use them. You will use them on long-lived objects or types holding large amounts of data where copies are expensive. Next, let's explore structs.