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 (22 chapters)
1
Section 1: Getting Up and Running with Go
10
Section 2: Instrumenting, Observing, and Responding
14
Section 3: Cloud ready Go

Getting to know about structs

Structs represent a collection of variables. In the real world, we work with data all the time that would be well represented by a struct. For example, any form that is filled out in a job application or a vaccine card is a collection of variables (for example, last name, first name, and government ID number) that each has types (for example, string, int, and float64) and are grouped together. That grouping would be a struct in Go.

Declaring a struct

There are two methods for declaring a struct. The first way is uncommon except in tests, as it doesn't allow us to reuse the struct's definition to create more variables. But, as we will see it later in tests, we will cover it here:

var record = struct{
     Name string
     Age int
}{
     Name: "John Doak",
     Age: 100, // Yeah, not publishing the real one
}

Here, we created a struct that contains two fields:

  • Name (string)
  • Age (int)

We then created an instance of that struct that has those values set. To access those fields, we can use the dot . operator:

fmt.Printf("%s is %d years old\n", record.Name, record.Age)

This prints "John Doak is 100 years old".

Declaring single-use structs, as we have here, is rarely done. Structs become more useful when they are used to create custom types in Go that are reusable. Let's have a look at how we can do that next.

Declaring a custom type

So far, we have created a single-use struct, which generally is not useful. Before we talk about the more common way to do this, let's talk about creating custom types.

Up until this point, we've seen the basic and pointer-wrapped types that are defined by the language: string, bool, map, and slice, for example. We can create our own types based on these basic types using the type keyword. Let's create a new type called CarModel that is based on the string type:

type CarModel string

CarModel is now its own type, just like string. While CarModel is based on a string type, it is a distinct type. You cannot use CarModel in place of a string or vice versa.

Creating a variable of CarModel can be done similar to a string type:

var myCar CarModel = "Chevelle"

Or, by using type conversion, as shown here:

myCar = CarModel("Chevelle") 

Because CarModel is based on string, we can convert CarModel back to string with type conversion:

myCarAsString := string(myCar)

We can create new types based on any other type, including maps, slices, and functions. This can be useful for naming purposes or adding custom methods to a type (we will talk about this in a moment).

Custom struct types

The most common way to declare a struct is using the type keyword. Let's create that record again, but this time let's make it reusable by declaring a type:

type Record struct{
     Name string
     Age int
}
func main() {
     david := Record{Name: "David Justice", Age: 28}
     sarah := Record{Name: "Sarah Murphy", Age: 28}
     fmt.Printf("%+v\n", david)
     fmt.Printf("%+v\n", sarah)
}

By using type, we have made a new type called Record that we can use again and again to create variables holding Name and Age.

Note

Similar to how you may define two variables with the same type on a single line, you may do the same within a struct type, such as First, Last string.

Adding methods to a type

A method is similar to a function, but instead of being independent, it is bound to a type. For example, we have been using the fmt.Println() function. That function is independent of any variable that has been declared.

A method is a function that is attached to a variable. It can only be used on a variable of a type. Let's create a method that returns a string representation of the Record type we created earlier:

type Record struct{
     Name string
     Age int
}
// String returns a csv representing our record.
func (r Record) String() string {
     return fmt.Sprintf("%s,%d", r.Name, r.Age)
}

Notice func (r Record), which attaches the function as a method onto the Record struct. You can access the fields of Record within this method by using r.<field>, such as r.Name or r.Age.

This method cannot be used outside of a Record object. Here's an example of using it:

john := Record{Name: "John Doak", Age: 100}
fmt.Println(john.String())

Let's look at how we change a field's value.

Changing a field's value

Struct values can be changed by using the variable attribute followed by = and the new value. Here is an example:

myRecord.Name = "Peter Griffin"
fmt.Println(myRecord.Name) // Prints: Peter Griffin

It is important to remember that a struct is not a reference type. If you pass a variable representing a struct to a function and change a field in the function, it will not change on the outside. Here is an example:

func changeName(r Record) {
     r.Name = "Peter"
     fmt.Println("inside changeName: ", r.Name)
}
func main() {
     rec := Record{Name: "John"}
     changeName(rec)
     fmt.Println("main: ", rec.Name)
}

This will output the following:

Inside changeName: Peter 
Main: John

As we learned in the section on pointers, this is because the variable is copied, and we are changing the copy. For struct types that need to have fields that change, we normally pass in a pointer. Let's try this again, using pointers:

func changeName(r *Record) {
	r.Name = "Peter"
	fmt.Println("inside changeName: ", r.Name)
}
func main() {
	// Create a pointer to a Record
	rec := &Record{Name: "John"}
	changeName(rec)
	fmt.Println("main: ", rec.Name)
}
Inside changeName: Peter
Main: Peter

This will output the following:

Inside changeName: Peter 
Main: Peter

Note that . is a magic operator that works on struct or *struct.

When I declared the rec variable, I did not set the age. Non-set fields are set to the zero value of the type. In the case of Age, which is int, this would be 0.

Changing a field's value in a method

In the same way that a function cannot alter a non-pointer struct, neither can a method. If we had a method called IncrAge() that increased the age on the record by one, this would not do what you wanted:

func (r Record) IncrAge() {
     r.Age++
}

The preceding code passes a copy of Record, adds one to the copy's Age, and returns.

To actually increment the age, simple make Record a pointer, as follows:

func (r *Record) IncrAge() {
     r.Age++
}

This will work as expected.

Tip

Here is a basic rule that will keep you out of trouble, especially when you are new to the language. If the struct type should be a pointer, then make all methods pointer methods. If it shouldn't be, then make them all non-pointers. Don't mix and match.

Constructors

In many languages, constructors are specially-declared methods or syntax that are used to initialize fields in an object and sometimes run internal methods as setup. Go doesn't provide any specialized code for that, instead, we use a constructor pattern using simple functions.

Constructors are commonly either called New() or New[Type]() when declaring a public constructor. Use New() if there are no other types in the package (and most likely won't be in the future).

If we wanted to create a constructor that made our Record from the previous section, it might look like the following:

func NewRecord(name string, age int) (*Record, error) {
     if name == "" {
          return nil, fmt.Errorf("name cannot be the empty string")
     }
     if age <= 0 {
          return nil, fmt.Errorf("age cannot be <= 0")
     }
     return &Record{Name: name, Age: age}, nil
}

This constructor takes in a name and age argument and returns a pointer to Record with those fields set. If we pass bad values for those fields, it instead returns the pointer's zero value (nil) and an error. Using this looks like the following:

     rec, err := NewRecord("John Doak", 100)
     if err != nil {
          return err
     }

Don't worry about the error, as we will discuss it in the course of the book's journey.

By now, you have learned how to use struct, Go's base object type. This included creating a struct, creating custom structs, adding methods, changing field values, and creating constructor functions. Now, let's look at using Go interfaces to abstract types.