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

Comprehending Go interfaces

Go provides a type called an interface that stores any value that declares a set of methods. The implementing value must have declared this set of methods to implement the interface. The value may also have other methods besides the set declared in the interface type.

If you are new to interfaces, understand that they can be a little confusing. Therefore, we will take it one step at a time.

Defining an interface type

Interfaces are most commonly defined using the type keyword that we discussed in the earlier section on structs. The following defines an interface that returns a string representing the data:

type Stringer interface {
          String() string
}

Note

Stringer is a real type defined in the standard library's fmt package. Types that implement Stringer will have their String() method called when passed to print functions in the fmt package. Don't let the similar names confuse you; Stringer is the interface type's name, and it defines a method called String() (which is uppercase to distinguish it from the string type, which is lowercase). That method returns a string type that should provide some human-readable representation of your data.

Now, we have a new type called Stringer. Any variable that has the String() string method can be stored in a variable of type Stringer. The following is an example:

type Person struct {
     First, Last string
}
func (p Person) String() string {
     return fmt.Sprintf("%s,%s", p.Last, p.First)
}

Person represents a record of a person, first and last name. We define String() string on it, so Person implements Stringer:

type StrList []string
func (s StrList) String() string {
     return strings.Join(s, ",")
}

StrList is a slice of strings. It also implements Stringer. The strings.Join() function used here takes a slice of strings and creates a single string with each entry from the slice separated by a comma:

// PrintStringer prints the value of a Stringer to stdout.
func PrintStringer(s Stringer) {
     fmt.Println(s.String())
}

PrintStringer() allows us to print the output of Stringer.String() of any type that implements Stringer. Both the types we created above implement Stringer.

Let's see this in action:

func main() { 
    john := Person{First: "John", Last: "Doak"} 
    var nameList Stringer = StrList{"David", "Sarah"} 
    PrintStringer(john)     // Prints: Doak,John 
    PrintStringer(nameList) // Prints: David,Sarah 
} 

Without interfaces, we would have to write a separate Print[Type] function for every type we wanted to print. Interfaces allow us to pass values that can do common operations defined by their methods.

Important things about interfaces

The first thing to note about interfaces is that values must implement every method defined in the interface. Your value can have methods not defined for the interface, but it doesn't work the other way.

Another common issue new Go developers encounter is that once the type is stored in an interface, you cannot access its fields, or any methods not defined on the interface.

The blank interface – Go's universal value

Let's define a blank interface variable: var i interface{}. i is an interface with no defined methods. So, what can you store in that?

That's right, you can store anything.

interface{} is Go's universal value container that can be used to pass any value to a function and then figure out what it is and what to do with it later. Let's put some things in i:

i = 3
i = "hello world"
i = 3.4
i = Person{First: "John"}

This is all legal because each of those values has types that define all the methods that the interface defined (which were no methods). This allows us to pass around values in a universal container. This is actually how fmt.Printf() and fmt.Println() work. Here are their definitions from the fmt package:

func Println(a ...interface{}) (n int, err error)
func Printf(format string, a ...interface{}) (n int, err error)

However, as the interface did not define any methods, i is not useful in this form. So, this is great for passing around values, but not using them.

Note about interface{} in 1.18:

Go 1.18 has introduced an alias for the blank interface{}, called any. The Go standard library now uses any in place of interface{}. However, all packages prior to 1.18 will still use interface{}. Both are equivalent and can be used interchangeably.

Type assertion

Interfaces can have their values asserted to either another interface type or to their original type. This is different than type conversion, where you change the type from one to another. In this case, we are saying it already is this type.

Type assertion allows us to change an interface{} value into a value that we can do something with.

There are two common ways to do this. The first uses the if syntax, as follows:

if v, ok := i.(string); ok {
     fmt.Println(v)
}

i.(string) is asserting that i is a string value. If it is not, ok == false. If ok == true, then v will be the string value.

The more common way is with a switch statement and another use of the type keyword:

switch v := i.(type) {
case int:
     fmt.Printf("i was %d\n", i)
case string:
     fmt.Printf("i was %s\n", i)
case float:
     fmt.Printf("i was %v\n", i)
case Person, *Person:
     fmt.Printf("i was %v\n", i)
default:
     // %T will print i's underlying type out
     fmt.Printf("i was an unsupported type %T\n", i)
}

Our default statement prints out the underlying type of i if it did not match any of the other cases. %T is used to print the type information.

In this section, we learned about Go's interface type, how it can be used to provide type abstraction, and converting an interface into its concrete type for use.