Go for DevOps

By John Doak , David Justice
    What do you get with a Packt Subscription?

  • Instant access to this title and 7,500+ eBooks & Videos
  • Constantly updated with 100+ new titles each month
  • Breadth and depth in over 1,000+ technologies
  1. Free Chapter
    Chapter 1: Go Language Basics

About 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.

Publication date:
July 2022
Publisher
Packt
Pages
634
ISBN
9781801818896

 

Chapter 1: Go Language Basics

DevOps is a concept that has been floating around since the early 2000s. It is a popularization of an operations discipline that relies on programming skills with development psychology popularized by Agile.

Site reliability engineering (SRE) is now considered a subdiscipline of DevOps, though it is likely the precursor to DevOps and relies more heavily on software skills and Service - Level Obligation (SLO)/Service - Level Agreement (SLA) modeling.

During my early time at Google, like many of today's DevOps shops, we used Python heavily. I think C++ was too painful for many SREs, and we had Python celebrities in Guido van Rossum and Alex Martelli.

But, as time wore on, many of the groups working in Python started having scaling issues. This included everything from Python running out of memory (requiring us to hack in our own malloc) to the Global Interpreter Lock (GIL) preventing us from true multithreading. At scale, we found that the lack of static types was giving us an abundance of errors that should have been caught at compile time. This mirrored what production services had seen years before.

But, Python came with more than compile-time and service-scaling issues. Simply moving to a new version of Python in the fleet might cause a service to stop working. The Python version run on Google machines would often get upgraded and expose bugs in your code that the previous version did not. Unlike a compiled binary, you could not just roll back to an old version.

Several of us in different organizations were looking to solve these types of problems without having to use C++. For my personal journey, I heard about Go from my colleagues in our Sydney office (Hey, Ross!). It was the pre-1.0 days, but they said it was already showing a lot of promise. I can't say I was even remotely convinced that what we needed was another language.

About 6 months later, however, I had bought Go hook, line, and sinker. It had everything we needed without everything we didn't. Now, it was still pre-1.0 days, so there was a certain amount of churn back then that was unpleasant (such as finding that Russ Cox had changed the time package over the weekend, so I had to rewrite a bunch of code). But, the benefits after writing my first service were undeniable.

I spent the next 4 years moving my department from a complete Python shop to almost a complete Go shop. I started teaching classes in Go across the world, targeted at operations engineers, rewriting core libraries for Go, and evangelizing to what was probably an annoying amount. Just because Go was invented at Google, it didn't mean that the engineers wanted to throw away their Python code and learn something new; there was more than a little resistance.

Now, Go has become the de facto language for cloud orchestration and software in the larger world (from Kubernetes to Docker). Go comes with all the tools you need to make huge strides in the reliability of your tooling and ability to scale.

Because many of these cloud services are written in Go, their parts are available to you by accessing their packages for your own tooling needs. This can make writing tooling for the cloud an easier experience.

For the next two chapters, I will be sharing my 10+ years' experience of teaching Go to engineers around the world to give you the basics and essentials of the Go language. Much of what you will read here is based on my free Go basics video training course, https://www.golangbasics.com. This course will differ slightly from that one in that it is more condensed. As you work your way through the book, we will continue to extend your knowledge of the Go language's standard library and third-party packages.

This chapter will cover the following main topics:

  • Using the Go Playground
  • Utilizing Go packages
  • Using Go's variable types
  • Looping in Go
  • Using conditionals
  • Learning about functions
  • Defining public and private
  • Using arrays and slices
  • Getting to know about structs
  • Understanding Go pointers
  • Comprehending Go interfaces

Now, let's get the basics down and get you on your way!

 

Technical requirements

The only technical requirement for this chapter is a modern web browser for using the Go Playground.

 

Using the Go Playground

The Go Playground, which you can find at https://play.golang.org/, is an online code editor and compiler that allows you to run Go code without installing Go on your machine. This is the perfect tool for our introductory chapters, allowing you to save your work online without the initial fuss of installing the Go tooling, or finding a code editor, for example.

There are four important parts of the Go Playground:

  • The code editing pane
  • The console window
  • The Run button
  • The Share button

The code editing pane, which is the yellow portion of the page, allows you to type in the Go code for your program. When you hit the Run button, the code will be compiled and then run with the output sent to the console, which is the white portion of the page below the code editor.

The following screen shows a glimpse of what the Go Playground does:

Figure 1.1 – Go Playground code editor

Figure 1.1 – Go Playground code editor

Clicking the Share button will store an immutable copy of the code and will change the URL from play.golang.org into a shareable link, such as play.golang.org/p/HmnNoBf0p1z. This link is a unique URL that you can bookmark and share with others. The code in this link cannot be changed, but if you hit the Share button again, it will create a new link with any changes.

Later chapters, starting with Chapter 4, Filesystem Interaction, will require installing the Go tooling for your platform.

This section taught you about the Go Playground and how to use it to write, view, share, and run your Go code. The Playground will be used extensively throughout the book to share runnable code examples.

Now, let's jump into writing Go code, starting with how Go defines packages.

 

Utilizing Go packages

Go provides reusable blocks of code that can be imported into other code using packages. Packages in Go are synonymous with libraries or modules in other languages. Packages are the building blocks of Go programs that divide the content into understandable parts.

This section will cover how to declare and import a package. We will discuss how to deal with package name conflicts, explore rules around packages, and we will write our first main package.

Declaring a package

Go divides programs into packages, sometimes called modules or libraries in other languages. Packages live on a path, and the path is made to look like a path to a directory on a Unix-like filesystem.

All Go files in a directory must belong to the same package. The package is most commonly named the same as the directory it lives in.

Declaring a package happens at the top of the file, and should only be preceded by a comment. Declaring a package is as simple as the following:

// Package main is the entrance point for our binary.
// The double slashes provides a comment until the end of the line.
/*
This is a comment that lasts until the closing star slash.
*/
package main

package main is special. All other package names declare a package that must be imported into another package to be used. package main will declare func main(), which is the starting point for a binary to run.

All Go files in a directory must have the same package header (compiler-enforced). These files, for most practical purposes, act as if they are concatenated together.

Let's say you have a directory structure as follows:

mypackage/
  file1.go
  file2.go

Then, file1.go and file2.go should have the following:

package mypackage

When mypackage is imported by another package, it will include everything declared in all files in the mypackage directory.

Importing a package

There are two general types of packages:

  • The standard library (stdlib) packages
  • All other packages

Standard library packages stand out because they don't list some repository information in their path, such as the following:

"fmt"
"encoding/json"
"archive/zip"

All other packages generally have repository information preceding them, as follows:

"github.com/johnsiilver/golib/lru"
"github.com/kylelemons/godebug/pretty"

Note

A complete listing of stdlib packages can be found at the following link: https://golang.org/pkg/.

To import packages, we use the import keyword. So, let's import the standard library fmt package and the mypackage package, which lives at github.com/devopsforgo/mypackage:

package main
import (
     "fmt"
     "github.com/devopsforgo/mypackage"
)

It is important to note that the filenames are not part of the package path, but simply the directory path.

Using a package

Once you've imported a package, you can start accessing functions, types, or variables declared in the package by prefacing what you want to access with the name of the package and a period.

For example, the fmt package has a function called Println() that can be used to print a line to stdout. If we want to use it, it is as simple as the following:

fmt.Println("Hello!")

Package name conflicts

Let's say you have two packages named mypackage. They both have the same name, so our program won't be able to tell which one we are referring to. You can rename a package import into whatever name you want:

import(
     "github.com/devopsforgo/mypackage"
     jpackage "github.com/johnsiilver/mypackage"
)

jpackage declares that in this package, we will refer to github.com/johnsiilver/mypackage as jpackage.

This ability allows us to use two similarly named packages as follows:

mypackage.Print()
jpackage.Send()

Now, we will look at an important rule around packages that improves compile-time and binary size.

Packages must be used

Let's introduce you to the following rule: If you import a package, you must use it.

One of the things that the Go authors noticed about many of the other programming languages being used at Google was that they often had unused imports.

This was leading to compile times that were longer than needed and, in some cases, binary sizes that were much bigger than required. Python files were packaged in a proprietary format to ship around production, and some of these unused imports were adding hundreds of megabytes to the files.

To prevent these types of problems, Go will not compile a program that imports a package but doesn't use it, as shown here:

package main
import (
     "fmt"
     "sync"
)
func main() {
     fmt.Println("Hello, playground")
}

The preceding code outputs the following:

./prog.go:5:2: imported and not used: "sync"

In certain rare circumstances, you may need to do a side effects import, in which just loading the package causes something to happen, but you don't use the package. This should always be done in package main and requires prepending with an underscore (_):

package main
import (
     "fmt"
     _ "sync" //Just an example 
)
func main() {
     fmt.Println("Hello, playground")
}

Next, we will declare a main package and discuss the basics of writing a Go program that imports a package.

A Go Hello World

Let's write a simple hello world program that is similar to the default program in the Go Playground. This example will demonstrate the following:

  • Declaring a package
  • Importing the fmt package from the standard library, which can print to our screen
  • Declaring the main() function of a program
  • Declaring a string variable using the := operator
  • Printing the variable to the screen

Let's see what this looks like:

1 package main
2 
3 import "fmt"
4
5 func main() {
6    hello := "Hello World!" fmt.Println(hello) 
7          
8 }

In our first line, we declared the name of our package using the package keyword. The entrance point for any Go binary is a package named main that has a function called main().

In our third line, we import the fmt package. fmt has functions for doing string formatting and writing to various outputs.

On our fifth line, we declare a function called main that takes no arguments and returns no values. main() is special, as when a binary is run, it starts by running the main() function.

Go uses {} to show where a function starts and where a function ends (similar to C).

The sixth line declares a variable named hello using the := operator. This operator indicates that we wish to create a new variable and assign it a value in a single line. This is the most common, but not the only, way to declare a variable.

As Go is typed, so := will assign the type based on the value. In this case, it will be a string, but if the value was an integer (such as 3), it would be the int type, and if a floating-point (such as 2.4), it would be the float64 type. If we wanted to declare a specific type, such as int8 or float32, we would need some modifications (which we will talk about later).

On the seventh line, we call a function that is in the fmt package called Println. Println() will print the contents of the hello variable to stdout followed by a new line character (\n).

You will notice that the way to use a function declared in another package is to use the package name (without quotes) + a period + the name of the function. In this case, fmt.Println().

In this section, you have learned how to declare a package, import a package, what the function of the main package is, and how to write a basic Go program with a variable declaration. In the next section, we will go into some depth on declaring and using variables.

 

Using Go's variable types

Modern programming languages are built with primitives called types. When you hear that a variable is a string or integer, you are talking about the variable's type.

With today's programming languages, there are two common type systems used:

  • Dynamic types (also called duck typing)
  • Static types

Go is a statically typed language. For many of you who might be coming from languages such as Python, Perl, and PHP, then those languages are dynamically typed.

In a dynamically typed language, you can create a variable and store anything in it. In those languages, the type simply indicates what is stored in the variable. Here is an example in Python:

v = "hello"
v = 8
v = 2.5

In this case, v can store anything, and the type held by v is unknown without using some runtime checks (runtime meaning that it can't be checked at compile time).

In a statically typed language, the type of the variable is set when it is created. That type cannot change. In this type of language, the type is both what is stored in the variable and what can be stored in the variable. Here is a Go example:

v := "hello" // also can do: var v string = "hello"

The v value cannot be set to any other type than a string.

It might seem like Python is superior because it can store anything in its variable. But in practice, this lack of being specific means that Python must wait until a program is running before it can find out there is a problem (what we call a runtime error). It is better to find the problem when the software is compiled than when it is deployed.

Let's take a look at a function to add two numbers together as an example.

Here is the Python version:

def add(a, b):
     return a+b

Here is the Go version:

func add(a int, b int) int {
     return a + b
}

In the Python version, we can see that a and b will be added together. But, what types are a and b? What is the result type? What happens if I pass an integer and a float or an integer and a string?

In some cases, two types cannot be added together in Python, which will cause a runtime exception, and you can never be sure of what the result type will be.

Note

Python has added type hints to the language to help avoid these problems. But, practical experience has taught us with JavaScript/Dart/TypeScript/Closure that while it can help, optional type support means that a lot of problems fall through the cracks.

Our Go version defines the exact types for our arguments and our result. You cannot pass an integer and a float or an integer and a string. You will only ever receive an integer as a return. This allows our compiler to find any errors with variable types when the program is compiled. In Python, this error could show up at any time, from the instant it ran to 6 months later when a certain code path was executed.

Note

A few years ago, there was a study done on the Rosetta Code repository for some of the top languages in use to see how they fared in processing time, memory use, and runtime failures. For runtime failures, Go had the least failures, with Python towards the bottom of the ranking. Static typing would have certainly played into that.

The study can be found here: https://arxiv.org/pdf/1409.0252.pdf.

Go's types

Go has a rich type system that not only specifies that a type might be an integer but also the size of the integer. This allows a Go programmer to reduce the size of a variable both in memory and when encoding for network transport.

The following table shows the most common types used in Go:

Table 1.1 – Common types used in Go and their descriptions

Table 1.1 – Common types used in Go and their descriptions

We will be keeping our discussion mostly to the preceding types; however, the following table is the full list of types that can be used:

Table 1.2 – Full list of types that you can use in Go

Table 1.2 – Full list of types that you can use in Go

Go doesn't just provide these types; you can also create new types based on these basic types. These custom types become their own type and can have methods attached to them.

Declaring a custom type is done with the type keyword and will be discussed during the section on the struct type. For now, we are going to move on to the basics of declaring variables.

Now that we've talked about our variable types, let's have a look at how we can create them.

Declaring variables

As in most languages, declaring a variable allocates storage that will hold some type of data. In Go, that data is typed so that only that type can be stored in the allocated storage. As Go has multiple ways to declare a variable, the next parts will talk about the different ways this can be done.

The long way to declare a variable

The most specific way to declare a variable is using the var keyword. You can use var to declare a variable both at the package level (meaning not inside a function) and within a function. Let's look at some examples of ways to declare variables using var:

var i int64

This declares an i variable that can hold an int64 type. No value is assigned, so the value is assigned the zero value of an integer, which is 0:

var i int = 3

This declares an i variable that can hold an int type. The value 3 is assigned to i.

Note that the int and int64 types are distinct. You cannot use an int type as an int64 type, and vice versa. However, you can do type conversions to allow interchanging these types. This is discussed later:

var (
     i int
     word = "hello"
)

Using (), we group together a set of declarations. i can hold an int type and has the integer zero value, 0. word doesn't declare the type, but it is inferred by the string value on the right side of the equal (=) operator.

The shorter way

In the previous example, we used the var keyword to create a variable and the = operator to assign values. If we do not have an = operator, the compiler assigns the zero value for the type (more on this later).

The important concept is as follows:

  • var created the variable but did not make an assignment.
  • = assigned a value to the variable.

Within a function (not at the package level), we can do a create and assign by using the := operator. This both creates a new variable and assigns a value to it:

i := 1                       // i is the int type 
word := "hello"              // word is the string type 
f := 3.2                     // f is the float64 type 

The important thing to remember when using := is that it means create and assign. If the variable already exists, you cannot use :=, but must use =, which just does an assignment.

Variable scopes and shadowing

A scope is the part of the program in which a variable can be seen. In Go, we have the following variable scopes:

  • Package scoped: Can be seen by the entire package and is declared outside a function
  • Function scoped: Can be seen within {} which defines the function
  • Statement scoped: Can be seen within {} of a statement in a function (for loop, if/else)

In the following program, the word variable is declared at the package level. It can be used by any function defined in the package:

package main
import "fmt"
var word = "hello"
func main() {
	fmt.Println(word)
}

In the following program, the word variable is defined inside the main() function and can only be used inside {} which defines main. Outside, it is undefined:

package main
import "fmt"
func main() {
	var word string = "hello"
	fmt.Println(word)
}

Finally, in this program, i is statement scoped. It can be used on the line starting our for loop and inside {} of the loop, but it doesn't exist outside the loop:

package main
import "fmt"
func main() {
	for i := 0; i < 10; i++ {
		fmt.Println(i)
	}
}

The best way to think of this is that if your variable is declared on a line that has {or within a set of {}, it can only be seen within those {}.

Cannot redeclare a variable in the same scope

The rule for this, You cannot declare two variables with the same name within the same scope.

This means that no two variables within the same scope can have the same name:

func main() {
     var word = "hello"
     var word = "world"
     fmt.Println(word)
}

This program is invalid and will generate a compile error. Once you have declared the word variable, you cannot recreate it within the same scope. You can change the value to a new value, but you cannot create a second variable with the same name.

To assign word a new value, simply remove var from the line. var says create variable where we want to only do an assignment:

func main() {
     var word = "hello"
     word = "world"
     fmt.Println(word)
}

Next, we will look at what happens when you declare two variables with the same name in the same scope, but within separate code blocks.

Variable shadowing

Variable shadowing occurs when a variable that is within your variable scope, but not in your local scope, is redeclared. This causes the local scope to lose access to the outer scoped variable:

package main
import "fmt"
var word = "hello"
func main() {
	var word = "world"
	fmt.Println("inside main(): ", word)
	printOutter()
}
func printOutter() {
	fmt.Println("the package level 'word' var: ", word)
}

As you can see, word is declared at the package level. But inside main, we define a new word variable, which overshadows the package level variable. When we refer to word now, we are using the one defined inside main().

printOutter() is called, but it doesn't have a locally shadowed word variable (one declared between its {}), so it used the one at the package level.

Here's the output of this program:

inside main():  world
the package level 'word' var:  hello

This is one of the more common bugs for Go developers.

Zero values

In some older languages, a variable declaration without an assignment has an unknown value. This is because the program creates a place in memory to store the value but doesn't put anything in it. So, the bits representing the value are set to whatever happened to be in that memory space before you created the variable.

This has led to many unfortunate bugs. So, in Go, declaring a variable without an assignment automatically assigns a value called the zero value. Here is a list of the zero values for Go types:

Table 1.3 – Zero values for Go types

Table 1.3 – Zero values for Go types

Now that we understand what zero values are, let's see how Go prevents unused variables in our code.

Function/statement variable must be used

The rule here is that if you create a variable within a function or statement, it must be used. This is much for the same reason as package imports; declaring a variable that isn't used is almost always a mistake.

This can be relaxed in much the same way as an import, using _, but is far less common. This assigns the value stored in someVar to nothing:

_ = someVar

This assigns the value returned by someFunc() to nothing:

_ = someFunc()

The most common use for this is when a function returns multiple values, but you only need one:

needed, _ := someFunc()

Here, we create and assign to the needed variable, but the second value isn't something we use, so we drop it.

This section has provided the knowledge of Go's basic types, the different ways to declare a variable, the rules around variable scopes and shadows, and Go's zero values.

 

Looping in Go

Most languages have a few different types of loop statements: for, while, and do while.

Go differs in that there is a single loop type, for, that can implement the functionality of all the loop types in other languages.

In this section, we will discuss the for loop and its many uses.

C style

The most basic form of a loop is similar to C syntax:

for i := 0; i < 10; i++ {
     fmt.Println(i)
}

This declares an i variable that is an integer scoped to live only for this loop statement. i := 0; is the loop initialization statement; it only happens once before the loop starts. i < 10; is the conditional statement; it happens at the start of each loop and must evaluate to true or the loop ends.

i++ is the post statement; it occurs at the end of every loop. i++ says to increment the i variable by 1. Go also has common statements, such as i += 1 and i--.

Removing the init statement

We don't need to have an init statement, as shown in this example:

var i int
for ;i < 10;i++ {
     fmt.Println(i)
}
fmt.Println("i's final value: ", i)

In this, we declared i outside the loop. This means that i will be accessible outside the loop once the loop is finished, unlike our previous example.

Remove the post statement too and you have a while loop

Many languages have a while loop that simply evaluates whether a statement is true or not. We can do the same by eliminating our init and post statements:

var i int
for i < 10 {
     i++
}
b := true
for b { // This will loop forever
     fmt.Println("hello")
}

You might be asking, how do we make a loop that runs forever? The for loop has you covered.

Creating an infinite loop

Sometimes you want a loop to run forever or until some internal condition inside the loop occurs. Creating an infinite loop is as simple as removing all statements:

for {
     fmt.Println("Hello World")
}

This is usually useful for things such as servers that need to process some incoming stream forever.

Loop control

With loops, you occasionally need to control the execution of the loop from within the loop. This could be because you want to exit the loop or stop the execution of this iteration of the loop and start from the top.

Here's an example of a loop where we call a function called doSomething() that returns an error if the loop should end. What doSomething()does is not important for this example:

for {
     if err := doSomething(); err != nil {
          break
     }
     fmt.Println("keep going")
}

The break function here will break out of the loop. break is also used to break out of other statements, such as select or switch, so it's important to know that break breaks out of the first statement it is nested inside of.

If we want to stop the loop on a condition and continue with the next loop, we can use the continue statement:

for i := 0; i < 10; i++ {
     if i % 2 == 0 { // Only 0 for even numbers
           continue
     }
     fmt.Println("Odd number: ", i)
}

This loop will print out the odd numbers from zero to nine. i % 2 means i modulus 2. Modulus divides the first number by the second number and returns the remainder.

Loop braces

Here is the introduction of this rule: A for loop’s open brace must be on the same line as the for keyword.

With many languages, there are arguments about where to put the braces for loops/conditionals. With Go, the authors decided to pre-empt those arguments with compiler checks. In Go, you can do the following:

for {
     fmt.Println("hello world")
}

However, the following is incorrect as the opening brace of the for loop is on its own line:

for
{
     fmt.Println("hello world")
}

In this section we learned to use for loops as C style loops, as while loops.

 

Using conditionals

Go supports two types of conditionals, as follows:

  • if/else blocks
  • switch blocks

The standard if statement is similar to other languages with the addition of an optional init statement borrowed from the standard C-style for loop syntax.

switch statements provide a sometimes-cleaner alternative to if. So, let's jump into the if conditional.

if statements

if statements start with a familiar format that is recognizable in most languages:

if [expression that evaluates to boolean] {
     ...
} 

Here's a simple example:

if x > 2 { 
    fmt.Println("x is greater than 2") 
}

The statements within {} in if will execute if x has a value greater than 2.

Unlike most languages, Go has the ability to execute a statement within the if scope before the evaluation is made:

if [init statement];[statement that evaluates to boolean] {
     ...
}

Here is a simple example that is similar to the init statement in a for loop:

if err := someFunction(); err != nil { 
    fmt.Println(err) 
}

Here, we initialize a variable called err. It has a scope of the if block. If the err variable does not equal the nil value (a special value that indicates certain types are not set – more on this later), it will print the error.

else

If you need to execute something when the condition of an if statement is not met, you can use the else keyword:

if condition {
     function1()
}else {
     function2()
}

In this example, if the if condition is true, function1 will be executed. Otherwise, function2 occurs.

It should be noted that most uses of else can generally be eliminated for cleaner code. If your if condition results in returning from a function using the return keyword, you can eliminate else.

An example is as follows:

if v, err := someFunc(); err != nil {
     return err
}else{
     fmt.Println(v)
     return nil
}

This can be simplified to the following:

v, err := someFunc()
if err != nil {
     return err 
}
fmt.Println(v)
return nil

Sometimes, you want to only execute code if the if condition is not met and another condition is. Let's look at that next.

else if

An if block can also contain else if, providing multiple levels of execution. The first if or else if that is matched in order is executed.

Note that often Go developers choose the switch statement as a cleaner version of this type of conditional.

An example is as follows:

if x > 0 {
     fmt.Println("x is greater than 0")
} else if x < 0 {
     fmt.Println("x is less than 0")
} else{
     fmt.Println("x is equal to 0")
}

Now that we have seen the basics of this conditional, we need to talk about brace style.

if/else braces

It's time to introduce this rule: Opening braces for if/else must be on the line with the associated keyword. If there is another statement in the chain, it must start on the same line as the previous close brace.

With many languages, there are arguments about where to put the braces for loops/conditionals.

With Go, the authors decided to pre-empt those arguments with compiler checks. In Go, you can't do the following:

if x > 0 
{ // This must go up on the previous line
     fmt.Println("hello")
}
else { // This line must start on the previous line
     fmt.Println("world")
}

So, with the arguments on bracing style in Go settled, let's look at an alternative to if/else, the switch statement.

The switch statement

switch statements are more elegant if/else blocks that are very flexible in their use. They can be used for doing exact matching and multiple true/false evaluations.

Exact match switch

The following is an exact match switch:

switch [value] {
case [match]:
     [statement]
case [match], [match]:
     [statement]
default:
     [statement]
}

[value] is matched against each case statement. If it matches, the case statement executes. Unlike some languages, once a match occurs, no other case is considered. If no match occurs, the default statement executes. The default statement is optional.

This has a nicer syntax than if/else for handling cases where your value can be several values:

switch x {
case 3:
     fmt.Println("x is 3")
case 4, 5:  // executes if x is 4 or 5
     fmt.Println("x is 4 or 5")
default:
     fmt.Println("x is unknown")
}

switch can also have an init statement, similar to if:

switch x := someFunc(); x {
case 3:
     fmt.Println("x is 3")
} 

True/false evaluation switch

We can also eliminate [match] so that each case statement isn't an exact match, but a true/false evaluation (as with if statements):

switch {
case x > 0:
     fmt.Println("x is greater than 0")
case x < 0:
     fmt.Println("x is less than 0")
default:
     fmt.Println("x must be 0")
}

At the end of this section, you should be able to use Go's conditional statements to branch code execution in your program based on some criteria and handle cases where no statement was matched. As conditionals are one of the standard building blocks of software, we will use these in many of the remaining sections.

 

Learning about functions

Functions in Go are what you'd expect from a modern programming language. There are only a few things that make Go functions different:

  • Multiple return values are supported
  • Variadic arguments
  • Named return values

The basic function signature is as follows:

func functionName([varName] [varType], ...) ([return value], [return value], ...){
}

Let's make a basic function that adds two numbers together and returns the result:

func add(x int, y int) int {
     return x + y
}

As you can see, this takes in two integers, x and y, adds them together, and returns the result (which is an integer). Let's show how we can call this function and print its output:

result := add(2, 2)
fmt.Println(result)

We can simplify this function signature by declaring both x and y types with a single int keyword:

func add(x, y int) int {
     return x + y
}

This is equivalent to the previous one.

Returning multiple values and named results

In Go, we can return multiple values. For example, consider a function that divides two integers and returns two variables, the result and the remainder, as follows:

func divide(num, div int) (res, rem int) {
	result = num / div
	remainder = num % div
	return res, rem
}

This code demonstrates a few new features in our function:

  • Argument num is the number to be divided
  • Argument div is the number to divide by
  • Return value res is the result of the division
  • Return value rem is the remainder of the division

First is named returns (res and rem). These variables are automatically created and ready for use inside the function.

Notice I use = and not := when doing assignments to those variables. This is because the variable already exists, and we want to assign a value (=). := means create and assign. You can only create a new variable that doesn't exist. You will also notice that now the return type is in parenthesis. You will need to use parenthesis if you use more than one return value or named returns (or in this case, both).

Calling this function is just as simple as calling add() before, as shown here:

result, remainder := divide(3, 2)
fmt.Printf("Result: %d, Remainder %d", result, remainder)

Strickly speaking, you don't have to use return to return the values. However, doing so will prevent some ugly bugs that you will eventually encounter.

Next, we will look at how we can have a variable number of arguments as function input that allows us to create functions such as fmt.Println(), which you have been using in this chapter.

Variadic arguments

A variadic argument is when you want to provide 0 to infinite arguments. A good example would be calculating a sum of integers. Without variadic arguments, you might use a slice (a growable array type, which we will talk about later), as follows:

func sum(numbers []int) int {
     sum := 0
     for _, n := range numbers {
          sum += n
     }
     return sum
}

While this is fine, using it is cumbersome:

args := []int{1,2,3,4,5}
fmt.Println(sum(args))

We can accomplish this same thing by using the variadic (...) notation:

func sum(numbers ...int) int {
     // Same code
}

numbers is still []int, but has a different calling convention that is more elegant:

fmt.Println(sum(1,2,3,4,5))

Note

You can use variadic arguments with other arguments, but it must be the last argument in the function.

Anonymous functions

Go has a concept of anonymous functions, which means a function without a name (also called a function closure).

This can be useful to take advantage of special statements that honor function boundaries, such as defer, or in goroutines. We will show how to take advantage of these for goroutines later, but for now let's show how to execute an anonymous function. This is a contrived example that is only useful in teaching the concept:

func main() {
     result := func(word1, word2 string) string {
          return word1 + " " + word2
     }("hello", "world")
     fmt.Println(result)
}

This code does the following:

  • Defines a single-use function (func(word1, word2 string) string)
  • Executes the function with the hello and world arguments
  • Assigns the string return value to the result variable
  • Prints result

Now that we have arrived at the end of this section, we have learned about how Go functions are declared, the use of multiple return values, variadic arguments for simplified function calling, and anonymous functions. Multiple return values will be important in future chapters where we deal with errors, and anonymous functions are key components of our future defer statements and for use with concurrency.

In the next section, we will explore public and private types.

 

Defining public and private

Many modern languages provide a set of options when declaring constants/variables/functions/methods that detail when a method can be called.

Go simplifies these visibility choices down to two types:

  • Public (exported)
  • Private (not exported)

Public types are types that can be referred to outside of the package. Private types can only be referred to inside the package. To be public, the constant/variable/function/method must simply start with an uppercase letter. If it starts with a lowercase letter, it is private.

There is a third type of visibility that we don't cover here: internally exported. This occurs when a type is public but in a package, located within a directory called internal/. Those packages can only be used by packages within a parent directory. You can read about this here: https://golang.org/doc/go1.4#internalpackages.

Let's declare a package and create some public and private methods:

package say
import "fmt"
func PrintHello() {
	fmt.Println("Hello")
}
func printWorld() {
	fmt.Println("World")
}
func PrintHelloWorld() {
	PrintHello()
	printWorld()
}

We have three function calls, two public (PrintHello() and PrintHelloWorld()) and one private (printWorld()). Now, let's create package main, import the say package, and call our functions:

package main
import "github.com/repo/examples/say"
func main() {
	say.PrintHello()
	say.PrintHelloWorld()
}

Now, let's compile and run it:

$ go run main.go
Hello
Hello
World

These work because PrintHello() and PrintHelloWorld() are both exported (public) functions. PrintHelloWorld() calls the private printWorld(), but that is legal because they are in the same package.

If we try to add say.printWorld() to func main() and run it, we will get the following:

./main.go:8:2: cannot refer to unexported name say.printWorld

Public and private apply to variables declared outside functions/methods and type declarations.

By the end of this short and sweet section, you've acquired the knowledge of Go's public and private types. This will be useful in code where you do not want to expose types in your public API. Next, we will look at arrays and slices.

 

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.

 

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.

 

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.

 

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.

 

Summary

In this chapter, you have learned the basics of the Go language. This includes variable types, functions, loops, methods, pointers, and interfaces. The skills acquired in this chapter provide the basic foundation needed to explore more advanced features of the Go language in our next chapter.

Next, we will be looking at essential capabilities of the Go language, such as handling errors, using concurrency, and Go's testing framework.

About the Authors

  • John Doak

    John Doak is the principal manager of Layer 1 Reliability Engineering at Microsoft. John led the development of the Azure Data Explorer and Microsoft Authentication Library Go SDKs. Previously, he was a Staff Site Reliability Engineer at Google. As part of network engineering, he created many of their first network automation systems. John led the migration of that group from Python to Go, developing Go training classes that have been taught around the world. He was a pivotal figure in transforming the network team to a network/systems group that integrated with SRE. Prior to that, he worked for Lucasfilm in video games and film. You can find his musings on Go/SRE topics and his Go classes on the web.

    Browse publications by this author
  • David Justice

    David Justice is the Principal Software Engineer Lead for the Azure K8s Infrastructure and Steel Thread teams that maintain a variety of CNCF and Byte Code Alliance projects. He is a maintainer of Cluster API Provider Azure and contributor to Cluster API. Prior to that, David was the technical assistant to the Azure CTO where he was responsible for Azure cross-group technical strategy and architecture. Early on at Microsoft, he was a Program Manager leading Azure SDKs and CLIs where he transitioned all Azure services to describe themselves using OpenAPI specifications in GitHub, and established automations to generate Azure reference docs, SDKs, and CLIs. Prior to Microsoft, David was the CTO of a mobile CI/CD SaaS called CISimple

    Browse publications by this author
Go for DevOps
Unlock this book and the full library FREE for 7 days
Start now