Skip to main content

System Tooling with Go

Course

Intro Video

Photo of Keith Thompson

Keith Thompson

DevOps Training Architect II in Content

Length

06:42:59

Difficulty

Beginner

Videos

24

Hands-on Labs

3

Course Details

This course is designed to teach you enough Go to be able to write your own tools. Beyond the language, you'll learn how to leverage Go's robust standard library, third-party packages, and cover how to handle some of the most common tasks when writing tools/scripts.

By the time you've finished this course, you will be able to:

Read, write, and understand Go codeUtilize Go as a primary language for toolingDevelop Go projects from start to finishBuild cross-platform Go binaries

Download the Interactive Diagram here: https://interactive.linuxacademy.com/diagrams/SystemToolingGo.html

Syllabus

Introduction

Getting Started

Course Introduction

00:00:55

Lesson Description:

Go is a programming language that is growing in popularity and used by many modern DevOps and container-based technologies. This course is designed to get you started with Go by giving you a solid understanding of the language basics, teaching you how to get the most out of the tools provided by the language, and giving you an idea of how to handle some of the common situations that arise when creating your own tools.

About the Course Author

00:00:43

Lesson Description:

A little about me, Keith Thompson.

Course Features and Tools

00:04:54

Lesson Description:

It's important for you to understand the tools and resources available to you as a Linux Academy student. This video will cover the course features used by this course and other tools you have at your disposal, such as flash cards and course schedules.

Introduction to Go

The History and Benefits of Go

00:02:42

Lesson Description:

Before we start using Go, let's cover the where Go came from, its goals, and benefits. Documentation For This VideoPDF of video's slideshow

Environment Setup

Installing Go on Unix Systems

00:13:37

Lesson Description:

Before we can actually utilize Go we're going to need to install it on our workstation. In this lesson, we'll go through the very straightforward approach used to install Go. Documentation For This VideoOfficial Go Installation DocumentationGo Package ArchivesGo's "How to Write Go Code" Document Installing Go on Unix Systems One of the big themes that we'll find as we work with Go is that Go tools can be distributed as static binaries and Go itself works the same way. Depending on the system that you're using, you need to find the corresponding package from the Go downloads page and unpackage its contents into the /usr/local directory on your machine. For this course, we'll be using a CentOS 7 cloud server so we'll need the linux-amd64 version of the package. Note: We'll be using Go 1.11.2 for this course.

[workstation] ~ $ cd /tmp
[workstation] /tmp $ curl -O https://dl.google.com/go/go1.11.2.linux-amd64.tar.gz
[workstation] /tmp $ tar -xzvf go1.11.2.linux-amd64.tar.gz
...
[workstation] /tmp $ sudo mv go /usr/local/
[workstation] /tmp $ cd ~
By unpacking this archive into the /usr/local/ directory we now have a /usr/local/go directory that will hold of the Go's internal parts. Adding Go Binaries to The $PATH Before we can actually use the Go tools we'll need to adjust our $PATH to include the binaries distributed as part of Go. We need to at the /usr/local/go/bin directory to our path by modifying our ~/.barshrc file: ~/.bashrc
# previous lines omitted
export PATH=$PATH:/usr/local/go/bin
Now we're able to run the go tool after reloading our .bashrc file:
[workstation] ~ $ exec $SHELL
[workstation] ~ $ go version
go version go1.11.2 linux/amd64
Setting Up the $GOPATH Go uses a very specific folder structure and environment variables to know where to find Go code. For the code that we'll be writing, we need to put them into a path that we store as the $GOPATH environment variable. We'll set this directory structure up at $HOME/go and set the $GOPATH within our .bashrc. Let's create the directory structure first:
[workstation] ~ $ mkdir -p $HOME/go/{bin,src}
Each of these directories stores something specific to Go:$GOPATH/src - contains Go source files$GOPATH/bin - contains executables Next, let's set the environment variable in our .bashrc: ~/.bashrc
# previous lines omitted
export GOPATH=$HOME/go
export PATH=$PATH:/usr/local/go/bin:$GOPATH/bin
Setting Up Other Tools We don't need very many other tools for our development, but there are some that we'll need:Git Let's install these now (on CentOS):
[workstation] ~ $ sudo yum install -y git
Customizing Our Text Editor Go is a compiled language that has some very powerful tools built for developing with it. Depending on the text editor that you're using you'll probably want to start by installing some tools to make development easier. Here are some of the popular ones:VSCode Govim-goGoLand (IDE by JetBrains) For this course, the lectures will be using Vim, but feel free to follow along with whatever tool you'd like. Here are the steps that I'm taking to set up my development environment. The vim-go package requires a newer version of Vim than is available on CentOS 7 systems so the first thing to do upgrade to Vim 8. This will require us to install some more development tools and remove the current installation of Vim.
[workstation] $ sudo yum autoremove -y vim
...
[workstation] $ sudo yum groupinstall -y "Development Tools"
...
[workstation] $ sudo yum install -y python36
...
[workstation] $ git clone https://github.com/vim/vim.git
[workstation] $ cd vim/src
[workstation] src $ make -j8
...
[workstation] src $ sudo make install
...
[workstation] src $ sudo cp vim /usr/bin/
[workstation] src $ cd ~
Next, we'll install the vim-plug package manager:
[workstation] $ curl -fLo ~/.vim/autoload/plug.vim --create-dirs 
    https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim
From here, I'll modify my ~/.vimrc file to use the package manager to install vim-go. ~/.vimrc
call plug#begin('~/.vim/plugged')

Plug 'fatih/vim-go'

call plug#end()

filetype off
filetype plugin indent on

set number
set noswapfile
set noshowmode
set ts=2 sw=2 sts=2 et
set backspace=indent,eol,start

" Map <leader> to comma
let mapleader=","

if has("autocmd")
  autocmd FileType go set ts=2 sw=2 sts=2 noet nolist autowrite
endif
All that is remaining now is for us to install the vim-go package and then have that package install the Go tools that it needs.
[workstation] ~ $ vim +PlugInstall +qa
...
[workstation] ~ $ vim +GoInstallBinaries +qa
...
Now we're ready to work with Go.

Hands-on Labs are real live environments that put you in a real scenario to practice what you have learned without any other extra charge or account to manage.

01:00:00

Just Enough Go

Running Go

Creating and Running Simple Go Programs

00:07:21

Lesson Description:

Now that our environment is set up, we're ready to write our first Go program. Documentation for This VideoHow to Write GoGo PackagesGo fmt PackageDiagram Creating Our Hello World Package For the most part, all Go code needs to be placed within a package, and packages are named according to where the project will live in source control. We'll see a lot of projects nested under $HOME/go/src/github.com/ with a variety of different user and organization names. To start, we're going to create a package at the root of our src directory called hello_world:

[workstation] src $ mkdir hello_world
[workstation] src $ cd hello_world
[workstation] hello_world $
Let's create our first Go file, main.go: ~/go/src/hello_world/main.go
package main

func main() {
}
Technically, this is a valid program that can be built into an executable, but it won't do anything. To create an executable, we need a main function defined within the main package within a package directory. To print to the screen when we run our program, we're going to need to use the fmt package from Go's standard library. Let's modify the file to actually print a message: ~/go/src/hello_world/main.go
package main

import "fmt"

func main() {
        fmt.Println("Hello Linux Academy!")
}
Now that we've imported the fmt package using the import keyword, we can access functions and types from within the package by chaining off of the package's name. The Println function takes strings and prints them out with a trailing newline character. Running Go Code Now that the file is written, we're ready to run our program. We're going to look at two different ways to do this:Compiling and running at the same time using go runCompiling a binary to run using go build The go run approach is great for testing something out without building a binary. Let's run our program now:
[workstation] hello_world $ go run main.go
Hello Linux Academy!
Note that when we use go run, we need to provide the file we want to run. The other approach, using go build, will create a binary for our program in our current directory. Let's see this in action:
[workstation] hello_world $ go build
[workstation] hello_world $ ./hello_world
Hello Linux Academy!

Using Comments

00:03:57

Lesson Description:

Leaving comments in code helps us (and others) understand complicated logic or how the code is supposed to be used. In this lesson, we'll take a look at how we can improve our code using comments. Documentation for This VideoThe Go WikiGo CommentsDiagram Comments Go supports single-line, multi-line, trailing, and GoDoc comments. Most of the comments that you'll see are created using two slashes (//), but occasionally you will see a multi-line comment that uses /* and */. Multi-line comments are not used very often in the Go community, so I recommend avoiding them. Here's an example that uses all of the different comment types: ~/go/src/hello_world/main.go

package main

import "fmt"

/*
this is a multi-line comment
but you usually don't use these
*/

// main is the primary function for our package.
// This function prints a message to the screen.
func main() {
        // this is a single line comment
        fmt.Println("Hello Linux Academy!") // this is a trailing comment
}
GoDoc GoDoc is a tool that can create documentation pages based on comments that exist within a package. If there are comments directly above something in Go source code (a function, package, etc.), GoDoc generates plain-text or HTML documentation tied to the GoDoc comments.

Common Data Types

Strings and Characters

00:09:49

Lesson Description:

To start learning Go we're going to work through the foundational data types that we have at our disposal while writing small programs along the way. In this lesson, we'll take a look at one of the types that we've already used, the string type or str as it is known in Go. Documentation For This VideoGo Lang Specification: String LiteralsGo Lang Specification: Rune LiteralsDiagram What is a String? We've already used a string in this course when we wrote our hello_world program because we needed something to print out to the screen. This is a string: "Hello Linux Academy!". Notices that it is created by using double quotes ("). Strings can also be created using backticks (`) if we need to utilize a double quote from within the string or we'd like to create a multi-line string. Some important things to note about strings in Go:The default value for a string is the empty string ""Strings are immutable so you cannot change a string after it has been created. This does not mean that we cannot create a new string from an existing one.Go strings are UTF-8 encoded so they are not restricted to containing ASCII characters. The best place to learn more about Go strings is within the Go Language Spec, but the spec can be a little academic to read. Exploring Strings with the Go Playground Unlike most interpreted programming languages there's no REPL (Read. Evaluate. Print. Loop) so if we're looking to explore language features. The most comparable thing that Go provides is The Go Playground which allows us to write some go in a browser editor and execute it with the push of a button. If you don't feel like using a browser though you can use go run with the file you're using to quickly execute it without compiling a new binary. Let's try to print out a few different strings using the fmt.Println function that we saw in the previous videos. ~/go/src/learning_strings.go

package main

import "fmt"

func main() {
    fmt.Println("Simple String")
    fmt.Println(`
This is a multi-line
String, that can also contain "quotes".
`)
    fmt.Println("?")
    fmt.Println("u2272")
}
Let's run this using go run:
[workstation] $ go run learning_strings.go
Simple String

This is a multi-line
String, that can also contain "quotes".

?
?
This shows a few of the interesting features of strings in Go. Since strings are UTF-8 encoded we can use emoji right in our scripts as well as using Unicode code points directly. Runes (or Characters) Notices that we have to use backticks (`) or double-quotes ("), but what if we use single quotes (')? Let's see what happens: ~/go/src/learning_strings.go
package main

import "fmt"

func main() {
    fmt.Println('LA')
}
Let's see what happens when we run the script:
[workstation] src $ go run learning_strings.go
# command-line-arguments
./learning_strings.go:6:14: invalid character literal (more than one character)
This error comes from the fact that the single quotes are used to denote individual characters and we can't use more than one in that situation. Let's make a modification and see what happens: ~/go/src/learning_strings.go
package main

import "fmt"

func main() {
    fmt.Println('L')
}
Running this one last time we'll see that it almost works as expected:
[workstation] src $ go run learning_strings.go
76

Numbers

00:09:17

Lesson Description:

With strings and characters under our belts, we're ready to move onto another core set of data types: Integers and Floats. Documentation For This VideoGo Lang Specification: Integer LiteralsGo Lang Specification: Floating Point LiteralsGo math PackageDiagram Working with Numbers It's incredibly common to need to perform math operations and work with numbers while writing programs and the two types that we need to use will be integers and floats. Let's take a look at how integer math works in Go (while stilling using fmt.Println): ~/go/src/learning_numbers.go

package main

import "fmt"

func main() {
    fmt.Println("Addition:", 1+3)
    fmt.Println("Subtraction:", 27-19)
    fmt.Println("Multiplication:", 9*11)
    fmt.Println("Division:", 20/4)
}
Running this we can see how the math is done and how a second argument is utilized by the fmt.Println function:
[workstation] src $ go run learning_numbers.go
Addition: 4
Subtraction: 8
Multiplication: 99
Division: 5
Notice that the final calculation is what is printed out and that the function automatically adds a space to separate the arguments. What happens when we perform division where there is a remainder? ~/go/src/learning_numbers.go
package main

import "fmt"

func main() {
    fmt.Println("Addition:", 1+3)
    fmt.Println("Subtraction:", 27-19)
    fmt.Println("Multiplication:", 9*11)
    fmt.Println("Division:", 20/3)
}
Let's see what the output is:
[workstation] src $ go run learning_numbers.go
Addition: 4
Subtraction: 8
Multiplication: 99
Division: 6
Go will not return a decimal number (float) when doing integer arithmetic. If we change any of the numbers in our expressions to be floats then the result will also be a float. ~/go/src/learning_numbers.go
package main

import "fmt"

func main() {
    fmt.Println("Addition:", 1.0+3)
    fmt.Println("Subtraction:", 27-19.5)
    fmt.Println("Multiplication:", 9.801*11)
    fmt.Println("Division:", 20/3.0)
}
Let's see the output again:
[workstation] src $ go run learning_numbers.go
Addition: 4
Subtraction: 7.5
Multiplication: 107.811
Division: 6.666666666666667
It's worth noting that when printing a number Go will truncate the 4.0 to 4. Additional Math Operations Some other types of operations that you might want to do with numbers in Go: modular division and exponents. Modular division does have an operator, the % operator, but there is no exponent operation. Let's take a look at how we would use exponents: ~/go/src/learning_numbers.go
package main

import (
    "fmt"
    "math"
)

func main() {
    fmt.Println("Addition:", 1.0+3)
    fmt.Println("Subtraction:", 27-19.5)
    fmt.Println("Multiplication:", 9.801*11)
    fmt.Println("Division:", 20/3.0)
    fmt.Println("Exponents", math.Pow(7, 3))
}
Here's our output:
[workstation] src $ go run learning_numbers.go
Addition: 4
Subtraction: 7.5
Multiplication: 107.811
Division: 6.666666666666667
Exponents: 343
Since there is not a shorthand way to work with exponents in Go so we needed to pull in the math package to use the math.Pow function.

Booleans and `nil`

00:05:24

Lesson Description:

The concepts of truth and nothingness are fairly important in programming and in this lesson we'll learn how Go handles those ideas. Documentation For This VideoGo Lang Specification: Boolean TypesGo Lang Specification: OperatorsDiagram Booleans and Comparison Operators We haven't talked about conditionals yet, but conditionals won't work for us later if we don't understand booleans first. The type for booleans in Go is bool, and most of the time you'll get this as a return type form a comparison operation. To demonstrate these comparisons we'll continue printing out values. Let's take a look at some of the operators now: ~/go/src/learning_booleans.go

package main

import "fmt"

func main() {
    fmt.Println("Greater than:", 1 > 2)
    fmt.Println("Less than:", 2 < 3)
    fmt.Println("Greater than OR equal to:", 2 >= 2)
    fmt.Println("Less than OR equal to:", 4 <= 4)
    fmt.Println("Equivalent:", 4.0 == 4)
    fmt.Println("Not equivalent:", 4.1 != 4.1)
}
There are other operators, but these are the main ones that we'll use in normal programming. Let's run this file to see the output:
[workstation] src $ go run learning_booleans.go
Greater than: false
Less than: true
Greater than OR equal to: true
Less than OR equal to: true
Equivalent: true
Not equivalent: false
Most of these work as we expect, but notice that we were able to compare a float with an integer and the comparison returned the result that we'd expect. Working with Nothingness The other unique type that we're going to look at right now is the one that represents nothingness and that's the nil value. We're not quite in a position to see nil in action just yet, but the most common place we'll run into this type is when there is an error returned by a function. If there is no error then this return value will be nil.

Working with Variables

00:08:47

Lesson Description:

We've seen the bulk of the low-level types that we'll work with as we start writing useful programs with Go, and now we're ready to move onto generally useful concepts. In this lesson, we'll cover the ways that we can declare variables in Go. Documentation For This VideoGo Lang Specification: Variable DeclarationGo Lang Specification: Short Variable DeclarationGo Lang Specification: Blank IdentifierDiagram Defining Variables the Verbose Way Go is a statically typed programming language, meaning that each variable has to have a type defined and the type of that variable cannot change. In other statically typed languages, this can make it a little annoying to define variables because you need to be explicit so that the compiler knows what is going on. We can use this same verbose approach when working with variables in Go so we'll look at how before we learn the shorthand approach. Let's create a new file at ~/go/src/learning_variables.go and continue working within the main function: ~/go/src/learning_variables.go

package main

import "fmt"

func main() {
    var myInt int = 16
    var val, ok = "yes", true

    fmt.Println("myInt is:", myInt)
    fmt.Println("myInt times two:", myInt*2)
    fmt.Println("val is:", val)
    fmt.Println("ok is:", ok)
}
When we run this file this is what we see:
[workstation] src $ go run learning_variables.go
myInt is: 16
myInt times two: 32
val is: yes
ok is: true
The structure for declaring a variable using the var keyword can go a few different ways:var identifier = value - This will work so long as the type of the value can be determined.var identifier type = value - This is what we've done in our function. The type of the value must match the type specified.var id1, id2, id3 type = val1, val2, val3 - This approach works if the values separated by commas all have the same type.var id1, id2 = val1, val2 - This is how our second declaration is working. val is a string and ok is a bool. One interesting thing about the Go compiler is that it won't allow you to declare a variable that is never used. To test this out, let's comment out the final fmt.Println in our file: ~/go/src/learning_variables.go
package main

import "fmt"

func main() {
    var myInt int = 16
    var val, ok = "yes", true

    fmt.Println("myInt is:", myInt)
    fmt.Println("myInt times two:", myInt*2)
    fmt.Println("val is:", val)
    //fmt.Println("ok is:", ok)
}
When we try to run the file now we should see an error:
[workstation] src $ go run learning_variables.go
# command-line-arguments
./learning_variables.go:7:11: ok declared and not used
We could fix this by not declaring the ok variable, but as we'll learn later, functions in Go can return multiple values and sometimes we only want to use one of the values that are returned. How would we get around this compiler error in that situation? Go gives us the blank identifier (_) that we can use in this situation to tell the compiler that we're intentionally not using this variable. Let's change our code to use the blank identifier now: ~/go/src/learning_variables.go
package main

import "fmt"

func main() {
    var myInt int = 16
    var val, _ = "yes", true

    fmt.Println("myInt is:", myInt)
    fmt.Println("myInt times two:", myInt*2)
    fmt.Println("val is:", val)
    //fmt.Println("ok is:", ok)
}
Now we should be able to run our program:
[workstation] src $ go run learning_variables.go
myInt is: 16
myInt times two: 32
val is: yes
Declaring a Variable and Assigning the Value Later In all of the examples so far we've declared the variable and assigned the value at the same time, but occasionally we're going to want to declare a variable that will be assigned later on. Let's take a look at how we could do this: ~/go/src/learning_variables.go
package main

import "fmt"

func main() {
    var name string
    var myInt int = 16
    var val, _ = "yes", true

    name = "Keith"

    fmt.Println("myInt is:", myInt)
    fmt.Println("myInt times two:", myInt*2)
    fmt.Println("val is:", val)
    fmt.Println("name is:", name)
}
Since we're not assigning the value right away we will need to always declare the type. When we go to assign the value later on the only thing that we need to use is the assignment operator (=) and the compiler will check to make sure that the previously declared type is the same as the type of the value on the right-hand side. Once again we can run the file:
[workstation] src $ go run learning_variables.go
myInt is: 16
myInt times two: 32
val is: yes
name is: Keith
Short Variable Declaration Using the var keyword really isn't that difficult or wordy, but Go does provide an operator to handle the declaration and assignment of a variable without using any keywords. This operator will look a little odd at first, but it is :=. Let's adjust our myInt variable to use this approach: ~/go/src/learning_variables.go
package main

import "fmt"

func main() {
    var name string
    var val, _ = "yes", true

    myInt := 16

    name = "Keith"

    fmt.Println("myInt is:", myInt)
    fmt.Println("myInt times two:", myInt*2)
    fmt.Println("val is:", val)
    fmt.Println("name is:", name)
}
Let's run the file one last time.
[workstation] src $ go run learning_variables.go
myInt is: 16
myInt times two: 32
val is: yes
name is: Keith

Arrays and Slices

00:13:03

Lesson Description:

Working with individual variables is very useful, but a lot of the work that we do requires us to work with data collections. In this lesson, we'll take a look at the two standard collection types that we work within Go: arrays and slices. Documentation For This VideoGo Lang Specification: Array TypesEffective Go: ArraysGo Lang Specification: Slice TypesEffective Go: SlicesDiagram Creating an Array Type In Go, Arrays are sequences of elements that are all of the same type. The types can be more complex than the primitive types that we've been using up to this point, but it's very common to work with those types. Let's create an array of strings called names in a new file at ~/go/src/learning_arrays.go: ~/go/src/learning_arrays.go

package main

import "fmt"

func main() {
    names := [3]string{"Keith", "Kevin", "Kayla"}

    fmt.Println(names)
}
Before we take a look at what's going on, let's run the file:
[workstation] src $ go run learning_arrays.go
[Keith Kevin Kayla]
Arrays always have a specified length and this length is actually used as part of the variable's type. The names variable has the type of [3]string. Within the { and } we've set the values for the three items, but we could have done this after the array was declared. Let's rewrite this so we can see how to set values within an array: ~/go/src/learning_arrays.go
package main

import "fmt"

func main() {
    var names [3]string
    names[0] = "Keith"
    names[1] = "Kevin"
    names[2] = "Kayla"

    fmt.Println(names)
}
Running this again will yield the same results as before. Arrays are zero indexed, so to get the first item we need to access the 0 index using names[0]. As we can see, to assign a value to the first position we can also pair the assignment operator with the subscript operation (the names[index] portion). There are a few more things that we should look at with arrays:What happens when we assign a value for the index of 3 (what would be the fourth item)?What happens when we don't assign a value for one of the indexes? Let's take a look at what happens when we try to assign a value for the 4th item: ~/go/src/learning_arrays.go
package main

import "fmt"

func main() {
    var names [3]string
    names[0] = "Keith"
    names[1] = "Kevin"
    names[2] = "Kayla"
    names[3] = "Kyle"

    fmt.Println(names)
}
And let's run it:
[workstation] src $ go run learning_arrays.go
# command-line-arguments
./learning_arrays.go:10:7: invalid array index 3 (out of bounds for 3-element array)
You probably saw that coming. Since the length is part of an array's type the compiler does a good job of determining if we're doing something that we shouldn't be. Accessing Array Values Array values are accessed using the subscript operation. We can use names[2] in an expression to utilize the value from names at the second index. What happens when we haven't assigned anything to that index though? Let's make some changes to see: ~/go/src/learning_arrays.go
package main

import "fmt"

func main() {
    var names [3]string
    names[0] = "Keith"
    names[1] = "Kevin"
    //names[2] = "Kayla"

    fmt.Println(names)
    fmt.Println("names[2] is nil:", names[2] == nil)
}
Let's run this to see what we get:
[workstation] src $ go run learning_arrays.go
# command-line-arguments
./learning_arrays.go:12:43: invalid operation: names[2] == nil (mismatched types string and nil)
We never specified the value for names[2] so shouldn't it be nil? In Go, the primitive types all have there own zero value. In the case of a string, the zero value is the empty string "". Let's change our comparison to see if we have an empty string: ~/go/src/learning_arrays.go
package main

import "fmt"

func main() {
    var names [3]string
    names[0] = "Keith"
    names[1] = "Kevin"
    //names[2] = "Kayla"

    fmt.Println(names)
    fmt.Println("names[2] is nil:", names[2] == "")
}
Running this we'll see that names[2] is indeed an empty string:
[workstation] src $ go run learning_arrays.go
[Keith Kevin ]
names[2] is nil: true
Slices: Variable Length Collections When we know exactly how many items we're going to have then arrays work great, but more often than not it seems like the number could vary a little. In this situation, we'll want to use a slice. Slices in Go are essentially variable length arrays. We'll still need to state the data type that the slice will hold, but we can adjust the number of items that are in the slice. Let's comment out the code that we have thus far and change the names variable to be a slice: ~/go/src/learning_arrays.go
package main

import "fmt"

func main() {
    //var names [3]string
    //names[0] = "Keith"
    //names[1] = "Kevin"
    //names[2] = "Kayla"
    //names[3] = "Kyle"

    names := []string{}
    names = append(names, "Keith")
    names = append(names, "Kevin", "Kayla", "Kyle")

    fmt.Println(names)
    fmt.Println("names[2] is nil:", names[2] == "")
}
Let's run this before we break down what is going on:
[workstation] src $ go run learning_arrays.go
[Keith Kevin Kayla Kyle]
names[2] is nil: false
Here we've defined the names slice to have a length of 0 to start and then we've appended items and reassigned the value of names using the built-in append function. Notice that we can append a single item or multiple and it still works as we'd expect. This approach works, but it requires us to allocate a new slice every time we insert an item. If we know what the starting length of a slice is going to be then we can use the make function to pre-allocate the space. Let's adjust the code to use make and the same assignment statements we were using when names was an array: ~/go/src/learning_arrays.go
package main

import "fmt"

func main() {
    //var names [3]string
    //names[0] = "Keith"
    //names[1] = "Kevin"
    //names[2] = "Kayla"
    //names[3] = "Kyle"

    //names := []string{}
    //names = append(names, "Keith")
    //names = append(names, "Kevin", "Kayla", "Kyle")

    names := make([]string, 4)
    names[0] = "Keith"
    names[1] = "Kevin"
    names[2] = "Kayla"
    names[3] = "Kyle"

    fmt.Println(names)
    fmt.Println("names[2] is nil:", names[2] == "")
}
Running this again would yield the same result as before. The make function takes a few arguments:The type that should be returned, in this case, a slice of strings ([]string).The initial length of the slice. By using the make function we were able to prevent the reallocating space and copying data around. Now we've got a basic grasp of how we can work with collections of similar types in Go.

Maps

00:06:49

Lesson Description:

Not all data collections are simply a list of items, occasionally we need to have unique identifiers for the items in the collection. In this situation, we want a key/value pair or a map in Go. In this lesson, we'll take a look at how we can create maps to represent various types of information. Documentation For This VideoGo Lang Specification: Map TypesEffective Go: MapsDiagram Creating a Map Type Maps in Go are similar to arrays in the sense that maps are created by specifying a map type that indicates the types for the keys and the values. Let's create a map to hold onto employee birthdays so that we can print them out (from within a new file ~/go/src/learning_maps.go): ~/go/src/learning_maps.go

package main

import "fmt"

func main() {
        birthdays := map[string]string{
                "Keith": "02/06/1990",
                "Kevin": "01/01/1957",
                "Kayla": "06/24/1975",
        }

        fmt.Println(birthdays)
}
Note: the , at the end of the line defining "Kayla" is required. Let's run this and take a look before we break the definition down:
[workstation] src $ go run learning_maps.go
map[Keith:02/06/1990 Kevin:01/01/1957 Kayla:06/24/1975]
It's important to note that unlike arrays or slices, we don't need to worry about the length of our map when we create it. We've created the type of map[string]string here's what this means:map - Simply means we're creating a map.[string] - We're specifying that the keys will be strings.string - The values will also be strings. Adding, Reading, and Removing Map Items Let's create another map for ages that will map names to ages so we can demonstrate how to add, read, and remove items from a map. ~/go/src/learning_maps.go
package main

import "fmt"

func main() {
        birthdays := map[string]string{
                "Keith": "02/06/1990",
                "Kevin": "01/01/1957",
                "Kayla": "06/24/1975",
        }

        fmt.Println(birthdays)

        ages := map[string]int{}
        ages["Keith"] = 28
        ages["Kevin"] = 61
        ages["Kayla"] = 43

        fmt.Println(ages, ages["Kevin"])
}
Similar to how we accessed an item at a specific index in an array or slice we use the subscript operation and the subscript assignment operation to add items to our map. If we had tried to assign a string for a value then we would see an error similar to this when we run it:
[workstation] src $ go run learning_maps.go
# command-line-arguments
./learning_maps.go:17:18: cannot use "61" (type string) as type int in assignment
Let's run the actual script to see what happens:
[workstation] src $ go run learning_maps.go
map[Kevin:01/01/1957 Kayla:06/24/1975 Keith:02/06/1990]
map[Kevin:61 Kayla:43 Keith:28] 61
The last thing that we need to know is how to remove an item from a map. Let's remove "Keith" from the birthdays map before we print it: ~/go/src/learning_maps.go
package main

import "fmt"

func main() {
        birthdays := map[string]string{
                "Keith": "02/06/1990",
                "Kevin": "01/01/1957",
                "Kayla": "06/24/1975",
        }

        delete(birthdays, "Keith")

        fmt.Println(birthdays)

        ages := map[string]int{}
        ages["Keith"] = 28
        ages["Kevin"] = 61
        ages["Kayla"] = 43

        fmt.Println(ages, ages["Kevin"])
}
The built-in [delete function][3] takes a map and the key for the item to remove. If the map is nil, or the value doesn't exist then the function simply doesn't do anything. Let's run this one last time:
[workstation] src $ go run learning_maps.go
map[Kevin:01/01/1957 Kayla:06/24/1975]
map[Kevin:61 Kayla:43 Keith:28] 61

Control Flow

Conditionals

00:10:45

Lesson Description:

We've covered all of the basic types that we'll need to use to write programs with Go. Now we're ready to learn a little about control flow by looking at how we can conditionally perform logic. Documentation For This VideoGo Lang Specification: If StatementsEffective Go: IfGo Lang Specification: Switch StatementsEffective Go: SwitchDiagram If Statements Running the exact same code every time isn't the most useful type of programming. It is good if you need to handle a repetitive task, but most of the time we're going to want to be able to fine tune what happens in our programs and work with some inputs to determine what needs to be done. For this to happen, we need to use conditionals and the most basic (and most commonly used) is the if statement. To this point, we've been printing out a lot of information and always doing it the same way, but we're finally to adjust what is being printed based on variables. For this lesson, let's create a new file at ~/go/src/learning_conditionals.go that is based on our learning_maps.go file:

[workstation] src $ cp learning_{maps,conditionals}.go
Let's edit the file to use some simple if statements: ~/go/src/learning_conditionals.go
package main

import "fmt"

func main() {
        ages := map[string]int{}
        ages["Kevin"] = 61

        if ages["Kevin"] < 67 {
                fmt.Println("Kevin is not of retirement age just yet")
        }
}
Running this we will see that Kevin isn't quite old enough yet:
[workstation] src $ go run learning_conditionals.go
Kevin is not of retirement age just yet
The if statement takes an expression that evaluates to a boolean value. If the value is true then the context under the if statement executes, but if the value is false that code will never run. In many cases, we want to always run something, just different things depending on whether the condition is true or false. For this situation, we can also add an else to our conditional. Let's print a different message if Kevin is over 65: ~/go/src/learning_conditionals.go
package main

import "fmt"

func main() {
        ages := map[string]int{}
        ages["Kevin"] = 67

        if ages["Kevin"] < 67 {
                fmt.Println("Kevin is not of retirement age just yet")
        } else {
                fmt.Println("Enjoy retirement Kevin!")
        }
}
If we've changed Kevin's age so that we will fall into the else block. Run the file again to see only one message printed.
[workstation] src $ go run learning_conditionals.go
Enjoy retirement Kevin!
Our conditional works great! It's worth noting that the expression doesn't need to be a comparison operation. We can use functions that return a boolean value also. The last situation we might run into is when we have 3 or more cases where we want to print a message based on something. Let's adjust our age conditional to handle people who aren't of voting age just yet. ~/go/src/learning_conditionals.go
package main

import "fmt"

func main() {
        ages := map[string]int{}
        ages["Kevin"] = 57

        if ages["Kevin"] < 18 {
                fmt.Println("Kevin can't vote yet")
        } else if ages["Kevin"] < 67 {
                fmt.Println("Kevin is not of retirement age just yet")
        } else {
                fmt.Println("Enjoy retirement Kevin!")
        }
}
The check for voting age needs to happen before we check about retirement age otherwise it would never be caught because a number less than 18 would also be less than 67. To use multiple conditionals we can utilize else if. Now if we change the value of Kevin's age there are three possible values that we might have printed out. Switch Statements It's technically possible to handle all types of conditionals using if, else if, and else, but it is occasionally nice to have a slightly different syntax if you need many else if blocks. For this, we can use the switch statement. Let's comment out our if/else if/else statement and write the same thing using a switch statement: ~/go/src/learning_conditionals.go
package main

import "fmt"

func main() {
        ages := map[string]int{}
        ages["Kevin"] = 12

        // if ages["Kevin"] < 18 {
        //      fmt.Println("Kevin can't vote yet")
        // } else if ages["Kevin"] < 67 {
        //      fmt.Println("Kevin is not of retirement age just yet")
        // } else {
        //      fmt.Println("Enjoy retirement Kevin!")
        // }

        switch {
        case ages["Kevin"] < 18:
                fmt.Println("Kevin can't vote yet")
        case ages["Kevin"] < 67:
                fmt.Println("Kevin is not of retirement age just yet")
        default:
                fmt.Println("Enjoy retirement Kevin!")
        }
}
We were doing "less than" comparisons in our if/else if/else statement, so we're using a switch statement without any expression here. This will evaluate the cases from top to bottom and if one is true then it will be executed, otherwise, the default block will run. In this situation, we would most often opt to use the standard if statement, but if we decided to use equality comparisons then the switch statement can be very powerful. Let's make an adjustment to see how equality comparisons can be made. ~/go/src/learning_conditionals.go
package main

import "fmt"

func main() {
        ages := map[string]int{}
        ages["Kevin"] = 11

        // if ages["Kevin"] < 18 {
        //      fmt.Println("Kevin can't vote yet")
        // } else if ages["Kevin"] < 67 {
        //      fmt.Println("Kevin is not of retirement age just yet")
        // } else {
        //      fmt.Println("Enjoy retirement Kevin!")
        // }

        switch ages["Kevin"] {
        case 1, 2, 3, 5, 7, 11, 13, 17, 19:
                fmt.Println("Kevin's age is a prime number!")
        case 16:
                fmt.Println("Kevin can drive now")
        case 18:
                fmt.Println("Kevin can vote now!")
        case 67:
                fmt.Println("Kevin can retire now!")
        default:
                fmt.Println("There's nothing special about Kevin's current age.")
        }
}
Running this as is you'd see that Kevin has a prime number birthday. This is much more interesting and would be pretty annoying to write as a traditional if chain. Let's take a look at what we changed:We moved ages["Kevin"] to be the expression of the if statement. This sets the expected type of all of the values in the case statements. In this case, ages["Kevin"] is an int so all of our cases need to be int.We've added a compound case statement that states if ages["Kevin"] matches any of the numbers in this list then print a message about it being a prime number.We're removed our "less than" comparisons and are only calling attention to voting and retirement ages if the age is an exact match.

The `for` Loop

00:12:05

Lesson Description:

When working with data collections, it's fairly common to want to process each item in the collection. To be able to do this we need to know how to use loops in Go. In this lesson, we'll discuss the only type of loop that exists in Go: the for loop. Documentation For This VideoGo Lang Specification: For StatementsEffective Go: ForThe fmt.Sprintf FunctionDiagram One Loop to Rule Them All Most programming languages have multiple statements that handle looping because there are different reasons why we might want to loop. Such as:To perform the same operation a specific number of timesTo perform the same operation as long as a condition is trueTo perform the same operation on each item in a collectionTo perform the same operation indefinitely (an infinite loop) Go still allows us to handle all of these cases, but we handle them all using the for statement. For this lesson, we're going to combine what we've learned up to this point to show how we can handle each of these looping cases. To start, let's create a copy of ~/go/src/learning_conditionals.go as ~/go/src/learning_loops.go.

[workstation] src $ cp learning_{conditionals,loops}.go
From here we're going to do the following:Add more people to the ages map.Loop through ages and process each person's age using our switch statement. ~/go/src/learning_loops.go
package main

import "fmt"

func main() {
        ages := map[string]int{}
        ages["Kevin"] = 11
        ages["Keith"] = 28
        ages["James"] = 67
        ages["Michael"] = 18
        ages["Leigha"] = 16

        for name, age := range ages {
                switch age {
                case 1, 2, 3, 5, 7, 11, 13, 17, 19:
                        fmt.Println(fmt.Sprintf("%s's age is a prime number!", name))
                case 16:
                        fmt.Println(name, "can drive now!")
                case 18:
                        fmt.Println(name, "can vote now!")
                case 67:
                        fmt.Println(name, "can retire now!")
                default:
                        fmt.Println(fmt.Sprintf("There's nothing special about %s's current age.", name))
                }
        }
}
In addition to the broad steps outlined above, we also had to do some more complicated string formatting to get each person's name in the proper spot for a few of the strings. We used the fmt.Sprintf function which returns a formatted string allowing us to substitute values in where we would like. Let's give this a shot before breaking down the for statement.
[workstation] src $ go run learning_loops.go
Michael can vote now!
Leigha can drive now!
Kevin's age is a prime number!
There's nothing special about Keith's current age.
James can retire now!
We were able to see all of the branches in our switch statement while only running the file one time! Loops are the last building block that we need to be able to write more complicated programs. Let's take a look at the for statement itself. Arguably the most common type of loop is one where you iterate over a sequence type (arrays, slices, and maps are all sequences). To do this, we use a range clause. It's worth noting that range is not function (which is why it doesn't have parenthesis) and it won't work outside the context of our for statement. The range clause takes a "range expression" that must evaluate to a sequence type and then for each item we do an assignment operation so that we have variables to use within the context of the loop. For a map, this will be both the key and the value, but for arrays and slices, it would be the index and the value at that index. Looping a Specific Number of Times Sometimes it is useful to be able to carry out a procedure a specific number of times. To do this we can use a different form of a for statement. From the specification we see this form defined as:
ForClause = [ InitStmt ] ";" [ Condition ] ";" [ PostStmt ] .
This looks a little gibberish at first, but it translates to:
for i := 1; i <= 10; i++ {
  // some procedure
}
Here are the parts:i := 1 is the InitStmt. This only happens once.i <= 10 is the Condition. This must be true for the loop to process the iteration for a given i.i++ is the PostStmt. This runs after each iteration and should make some modification to the variable created in the InitStmt and tested in the Condition. If you don't modify the value then the loop will run forever. We can use this approach to print out a few lines: ~/go/src/learning_loops.go
package main

import "fmt"

func main() {
        ages := map[string]int{}
        ages["Kevin"] = 11
        ages["Keith"] = 28
        ages["James"] = 67
        ages["Michael"] = 18
        ages["Leigha"] = 16

        for i := 1; i <= 10; i++ {
                fmt.Println("We're counting:", i)
        }

        for name, age := range ages {
                switch age {
                case 1, 2, 3, 5, 7, 11, 13, 17, 19:
                        fmt.Println(fmt.Sprintf("%s's age is a prime number!", name))
                case 16:
                        fmt.Println(name, "can drive now!")
                case 18:
                        fmt.Println(name, "can vote now!")
                case 67:
                        fmt.Println(name, "can retire now!")
                default:
                        fmt.Println(fmt.Sprintf("There's nothing special about %s's current age.", name))
                }
        }
}
Let's run it to make sure that it works:
[workstation] src $ go run learning_loops.go
We're counting: 1
We're counting: 2
We're counting: 3
We're counting: 4
We're counting: 5
We're counting: 6
We're counting: 7
We're counting: 8
We're counting: 9
We're counting: 10
Kevin's age is a prime number!
There's nothing special about Keith's current age.
James can retire now!
Michael can vote now!
Leigha can drive now!
Achieving a while Loop The final way that we might want to loop will be based on a condition. This is used when you want to execute the code within the loop a variable number of times, but whether or not the loop should continue is determined during the loop itself (usually within a conditional). Thankfully, this is easy with the final form of the for loop:
ForStmt = "for" [ Condition ] Block .
This means that to achieve a while loop equivalent from other languages we simply need to use a conditional as we would in an if statement. Here's an example of a conditional for loop: ~/go/src/learning_loops.go
package main

import "fmt"

func main() {
        ages := map[string]int{}
        ages["Kevin"] = 11
        ages["Keith"] = 28
        ages["James"] = 67
        ages["Michael"] = 18
        ages["Leigha"] = 16

        for i := 1; i <= 10; i++ {
                fmt.Println("We're counting:", i)
        }

        a := 0
        for a < 10 {
                fmt.Println("We're counting (again):", a)
        }

        for name, age := range ages {
                switch age {
                case 1, 2, 3, 5, 7, 11, 13, 17, 19:
                        fmt.Println(fmt.Sprintf("%s's age is a prime number!", name))
                case 16:
                        fmt.Println(name, "can drive now!")
                case 18:
                        fmt.Println(name, "can vote now!")
                case 67:
                        fmt.Println(name, "can retire now!")
                default:
                        fmt.Println(fmt.Sprintf("There's nothing special about %s's current age.", name))
                }
        }
}
Let's run this to see what happens:
[workstation] src $ go run learning_loops.go
We're counting (again): 0
We're counting (again): 0
We're counting (again): 0
...
This will keep repeating forever because we didn't change the value of a along the way. To stop this we need to hit Ctrl-C to send an interrupt to our program. We'll fix this loop while we learn about manual continuation and breaking. Manually Continuing or Stopping a Loop The last aspect of loops that we need to learn about is how to completely stop a loop before it has finished its iteration and how to continue to the next iteration if we know that we don't need to do anything with the current value. To demonstrate these features we're going to change our conditional loop to do the following:Not print if a is even.Completely stop the loop if a is 5. ~/go/src/learning_loops.go
package main

import "fmt"

func main() {
        ages := map[string]int{}
        ages["Kevin"] = 11
        ages["Keith"] = 28
        ages["James"] = 67
        ages["Michael"] = 18
        ages["Leigha"] = 16

        for i := 1; i <= 10; i++ {
                fmt.Println("We're counting:", i)
        }

        a := 0
        for a < 10 {
                if a%2 == 0 {
                        a++
                        continue
                } else if a == 5 {
                        break
                }

                fmt.Println("We're counting (again):", a)
                a++
        }

        for name, age := range ages {
                switch age {
                case 1, 2, 3, 5, 7, 11, 13, 17, 19:
                        fmt.Println(fmt.Sprintf("%s's age is a prime number!", name))
                case 16:
                        fmt.Println(name, "can drive now!")
                case 18:
                        fmt.Println(name, "can vote now!")
                case 67:
                        fmt.Println(name, "can retire now!")
                default:
                        fmt.Println(fmt.Sprintf("There's nothing special about %s's current age.", name))
                }
        }
}
Let's run this one last time:
[workstation] src $ go run learning_loops.go
We're counting: 1
We're counting: 2
We're counting: 3
We're counting: 4
We're counting: 5
We're counting: 6
We're counting: 7
We're counting: 8
We're counting: 9
We're counting: 10
We're counting (again): 1
We're counting (again): 3
Leigha can drive now!
Kevin's age is a prime number!
There's nothing special about Keith's current age.
James can retire now!
Michael can vote now!

Programming with Go

Basic Interactions

Reading the Documentation

00:03:05

Lesson Description:

Before we start working on more realistic programs, we want to go through what it looks like to read through the documentation for the standard library. Documentation For This VideoGo Lang Package DocumentationGo Lang strings PackageDiagram Reading and Discovering Standard Library Packages Go's standard library has a lot of fantastic packages that we'll want to rely on so that we don't have to implement a lot of common expected functionality. To get the most out of these packages though, we'll need to learn how to read the documentation. To start, let's take a look at the Go package listing on golang.org. This listing contains the standard library packages as well as some experimental or example packages maintained by the core team. In general, packages are grouped by the overall topic and a package like archive has two related subpackages of tar and zip, which are both related to working with archives of the corresponding name. If we take a look at the strings package, we'll see that package documentation tends to be broken down into a few sections:The import statement.An overview describing the package.The index of all of the functions, types, and constants exposed by the package.Examples of how to accomplish various things using the package. The import statement might seem obvious, but we can view other packages using this same documentation viewer later and third-party packages would show the full source control path that we would need to use in the import statement.

Function Basics

00:08:46

Lesson Description:

One of the best tools that we have to allow us to write good programs are functions. We'll rely on many standard library functions and also write our own functions to extract code. Documentation For This VideoGo Lang Specification: FunctionsEffective Go: FunctionsEffective Go: Pointers vs ValuesDiagram Understanding Function Types Every file that we've worked with up to this point has included a main function, and every main function has the same type. It receives no arguments and returns nothing. This is the simplest function type, but most functions take arguments and return data. Before we start working with more complicated functions we're going to create a new package to hold our code. We're going to create a CLI to modify a system's message of the day. Let's create a new directory called motd in our $GOPATH/src:

$ mkdir -p $GOPATH/src/motd
$ cd $GOPATH/src/motd
To start, we'll create a main function and a greeting function to build up and return a string in a main.go file: $GOPATH/src/motd/main.go
package main

import "fmt"

func main() {
  message := greeting("Keith", "Hello")
  fmt.Println(message)
}

func greeting(name string, message string) string {
  return fmt.Sprintf("%s, %s", message, name)
}
The greeting function takes two arguments that are both of the string type and it returns a single string value. Just like defining a variable, defining a function can take many forms. This same function could have been defined like this:
func greeting(name, message string) (salutation string) {
  salutation = fmt.Sprintf("%s, %s", message, name)
  return
}
We did a few things here:Since name and message have the same type, we compressed them in the function definition.We decided to name our return value as salutation. In doing this, we had a preallocated variable in our function and when we called return it returned the value of that variable. Exposing a Package's Functions The beauty of working with packages is that we can bundle up functions that deal with specific concerns. Creating a message could be considered one of these concerns. Let's create a new package within our motd package by creating a file called greeting.go within a new message directory and moving our greeting function over:
$ mkdir message
~/go/src/motd/message/greeting.go
package message

import "fmt"

func greeting(name, message string) string {
  return fmt.Sprintf("%s, %s", message, name)
}
We've indicated that this package has the name of message in the first line. It's worth noting that for a file to be runnable it needs to be for the main package. To use this function we will need to add another import statement to our main.go file. ~/go/src/motd/main.go
package main

import (
        "fmt"
        "motd/message"
)

func main() {
        m := message.greeting("Keith", "Hello")
        fmt.Println(m)
}
If we run this now we'll actually see an error:
$ go run main.go
# command-line-arguments
./main.go:9:7: cannot refer to unexported name message.greeting
./main.go:9:7: undefined: message.greeting
The error mentions that our message.greeting function is "unexported", and this is because it starts with a lowercase letter. There's not a "private" or "public" keyword in Go so everything that we define with a capitalized name is exported and can be used by other packages. Let's export our greeting function by renaming it to Greeting: ~/go/src/motd/message/greeting.go
package message

import "fmt"

func Greeting(name, message string) string {
  return fmt.Sprintf("%s, %s", message, name)
}
Now our main.go file will run.

Reading User Input

00:11:09

Lesson Description:

All of our code up to this point runs the same way every time and we're finally ready to read user input to create more dynamic programs. In this lesson, we'll see what it takes to prompt the user for input when we run our program. Documentation For This VideoGo os PackageGo os.Stdin VariableGo bufio PackageGo bufio.NewReader FunctionGo io.Reader InterfaceGo Reader.ReadString MethodGo strings.TrimSpace FunctionDiagram Reading Stdin Our message of the day (MOTD) program will eventually allow us to have a CLI for manipulating a system's MOTD and for us to do that we need to be able to take in some form of user information. To start, let's take a look at how we would read in Stdin to change the greeting that we're eventually printing. To do this we need a few things:The os.Stdin variable. This variable is an io.Reader (will explain in a second) that we can read from.The bufio.Reader struct. We'll create a new bufio.Reader that reads from Stdin and gives us a string.The Reader.ReadString method. The Reader.ReadString method is a function that is connected to the state within our bufio.Reader variable.The strings.TrimSpace function. The string we read from Stdin will contain a trailing space and we want to remove that before we create our message. ~/go/src/motd/main.go

package main

import (
        "bufio"
        "fmt"
        "motd/message"
        "os"
        "strings"
)

func main() {
        reader := bufio.NewReader(os.Stdin)
        fmt.Print("Your Greeting: ")
        phrase, _ := reader.ReadString('n')
        phrase = strings.TrimSpace(phrase)

        fmt.Print("Your Name: ")
        name, _ := reader.ReadString('n')
        name = strings.TrimSpace(name)

        m := message.Greeting(name, phrase)
        fmt.Println(m)
}
Now when we run this we'll be prompted for information:
$ go run main.go
Your Greeting: Howdy
Your Name: Keith
Howdy, Keith
Structs and Methods To write the code we needed, we had to use a more complex type than the built-in primitives in the form of the bufio.Reader. This is a struct and structs give us a few things:The ability to hold onto named variables within a parent item; these are commonly called attributes or properties.The ability to associate functions to this struct that access the internally stored attributes. This provides us with a lot of power and we'll use structs more and more as we continue through the course. Interfaces Now that we know what structs and methods are, we're able to dig into what an interface is. Sometimes we want to be able to classify multiple structs that can work for the same function. In our case, Stdin is an io.Reader because it can be read from, but it's not the only thing that we will interact with that is readable. For instance, a file is also readable. The io.Reader interface specifies the exact methods that a struct needs to have to be used in a function that requires an io.Reader.

Interacting with Files

00:13:24

Lesson Description:

We're reading user input now, and it's time for us to do something with the information besides print it back out to the screen. In this lesson, we'll learn how to write content to files. Documentation For This VideoGo os PackageGo io/ioutil PackageGo Lang Specification: ConversionsDiagram Writing a File with ioutil We interact with files a lot and it's a requirement for our MOTD program because we need to change the content of /etc/motd. There are a few ways of doing this so we'll take a look at them and things that we need to consider with each. To begin, we'll write our greeting to the /etc/motd file using ioutil.WriteFile because it's the simplest of our options. Let's add this step to our ~/go/src/motd/main.go file:

package main

import (
        "bufio"
        "fmt"
        "io/ioutil"
        "motd/message"
        "os"
        "strings"
)

func main() {
        reader := bufio.NewReader(os.Stdin)
        fmt.Print("Your Greeting: ")
        phrase, _ := reader.ReadString('n')
        phrase = strings.TrimSpace(phrase)

        fmt.Print("Your Name: ")
        name, _ := reader.ReadString('n')
        name = strings.TrimSpace(name)

        m := message.Greeting(name, phrase)
        _ = ioutil.WriteFile("/etc/motd", []byte(m), 0644)
}
Unfortunately, if we run this we won't have anything in our /etc/motd because we need to be the root user to edit this file. Our file will need to be run with sudo to work. We did need to convert a string into a slice of bytes ([]byte) and thankfully we can do that by using type conversion using []byte(m) and we'll be able to use the content of our string as an argument to the ioutil.WriteFile function. Handling Errors Why didn't our program fail when we ran it? The reason is that there aren't exceptions in Go and we chose to ignore the error returned by the ioutil.WriteFile function. Go doesn't have exceptions because we should know where errors can occur in our applications and handle them accordingly. We're expected to do something with this error if it exists, and that leads to this showing up in Go code a lot. In our case, we're going to print a message and exit with a non-zero exit status via os.Exit:
if err != nil {
  // do something to handle error
  fmt.Println("Unable to write /etc/motd")
  os.Exit(1)
}
Opening a File While using ioutil.WriteFile works great for our particular use case, we'll often want to read and write from the os.File struct. Here's what our code would look like if we opened the file and then wrote to it later:
package main

import (
        "bufio"
        "fmt"
        "motd/message"
        "os"
        "strings"
)

func main() {
        f, err := os.OpenFile("/etc/motd", os.O_WRONLY, 0644)

        if err != nil {
                fmt.Println("Error: Unable to open to /etc/motd")
                os.Exit(1)
        }

        defer f.Close()

        reader := bufio.NewReader(os.Stdin)
        fmt.Print("Your Greeting: ")
        phrase, _ := reader.ReadString('n')
        phrase = strings.TrimSpace(phrase)

        fmt.Print("Your Name: ")
        name, _ := reader.ReadString('n')
        name = strings.TrimSpace(name)

        m := message.Greeting(name, phrase)
        _, err = f.Write([]byte(m))

        if err != nil {
                fmt.Println("Error: Failed to write to /etc/motd")
                os.Exit(1)
        }
}
By using os.OpenFile we're able to give ourselves access to a os.File variable and have the flexibility to read from it, write to it, and utilize it with other functions. It is worth noting that we need to specify os.O_WRONLY (open with write-only mode) to be able to write to it and we currently couldn't read from it. The available values for this argument can be found in the os package's constants. To maintain the previous behavior we needed to use File.Write. This function also takes a byte slice so we didn't need to change much else. Running as Root We now know that we can't actually change the /etc/motd file without being root or using sudo. Let's try to run our program using sudo go run main.go:
$ sudo go run main.go
[sudo] password for cloud_user:
sudo: go: command not found
Why is this failing? Unlike a lot of the tools that we install, our Go tools are all installed inside /usr/local/go/bin which isn't part of the root user's $PATH. We have a few ways of getting around this:Build the CLI using go build and then run sudo ./motd.Add /usr/local/go/bin to the secure_path variable in the /etc/sudoers file. We're going to go with option 2:
$ sudo vim /etc/sudoers
/etc/sudoers
# previous omitted

Defaults    secure_path = /usr/local/rvm/gems/ruby-2.4.1/bin:/usr/local/rvm/gems/ruby-2.4.1@global/bin:/usr/local/rvm/rubies/ruby-2.4.1/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/usr/local/rvm/bin:/usr/local/go/bin

# remainder omitted
Note: We'll need to force the save using :w! because this file will open in read-only mode. Let's try to run our tool again:
$ sudo go run main.go
main.go:7:2: cannot find package "motd/message" in any of:
        /usr/local/go/src/motd/message (from $GOROOT)
        /root/go/src/motd/message (from $GOPATH)
Yet another error because our motd/message package is in our user's $GOPATH and not the root user's. We'll specify GOPATH in the command:
$ sudo GOPATH=$GOPATH go run main.go
Your Greeting: Howdy
Your Name: Keith
$

Building a CLI

Creating a CLI

00:18:51

Lesson Description:

Technically our MOTD tool works, but we would like to improve the usability. Most tools that we work with have some form of documentation using a help flag or options to modify how the tool does its job. In this lesson, we'll take a look at how we can build a more robust CLI using the Go standard library. Documentation For This VideoGo flag PackageDiagram Planning the CLI Our tool is currently pretty simple, but it does require a user to manually input information to Stdin. This limitation means that we can't use it in automation very easily. To get around this, we're going to add the ability to use flags for the "greeting" (required), "name" (required), and an optional "prompt" flag to use Stdin as we have been. Additionally, we'll add a "preview" flag to see the final message instead of writing it to the file. To handle these flags, we're going to use the flag package that is part of the standard library. We're getting to a point where our main function is getting a little lengthy and we should consider cleaning it up and separating out other operations for clarity's sake. Here's how we'd like the function to go (existing code omitted):

package main

// imports omitted

func main() {
  // Define flags

  // Parse flags

  // Show usage if flag combination isn't valid (exit in this scenario)

  // Optionally print flags and exit if DEBUG is set

  // Conditionally read from stdin

  // Generate message

  // Either preview message or write to file

  // Write content
}
Defining Flags To get started, we're going to declare some variables to hold our settings, but we won't assign any values to them.
func main() {
  var name string
  var greeting string
  var prompt bool
  var preview bool

  // Define flags

  // remainder omitted
}
To define our flags, we need to learn about pointers and how to work with them in Go. Every value that we store has a memory address, but most of the time we want to work with the value itself. There are times when we would rather pass around a reference to our variable so that a function can manipulate its state. In our case, we want to define our flags in such a way that, when parsed, they will populate our name, greeting, prompt, and preview variables. In Go, to reference the address of a variable, we prepend an ampersand to the variable name (example: &name). Let's define our flags now using the flag.StringVar and flag.BoolVar functions:
func main() {
  var name string
  var greeting string
  var prompt bool
  var preview bool

  // Define flags
  flag.StringVar(&name, "name", "", "name to use within the message")
  flag.StringVar(&greeting, "greeting", "", "phrase to use within the greeting")
  flag.BoolVar(&prompt, "prompt", false, "use prompt to input name and message")
  flag.BoolVar(&preview, "preview", false, "use preview to output message without writing to /etc/motd")

  // remainder omitted
}
Note: When a function expects a variable pointer, it will have an asterisk in front of the type. So *string means a pointer to a string variable. Make sure that you don't forget to import the flag package. Parsing Flags With our flags defined, we're ready to actually parse them and ensure that the values are set. Parsing the flags is pretty simple, we need to call flag.Parse(). After the parsing has completed we will have values in our variables. If all of the values match the default values, then we're going to want to print some help text and exit. Let's add all of this now:
func main() {
  var name string
  var greeting string
  var prompt bool
  var preview bool

  // Define flags
  flag.StringVar(&name, "name", "", "name to use within the message")
  flag.StringVar(&greeting, "greeting", "", "phrase to use within the greeting")
  flag.BoolVar(&prompt, "prompt", false, "use prompt to input name and message")
  flag.BoolVar(&preview, "preview", false, "use preview to output message without writing to /etc/motd")

  // Parse flags
  flag.Parse()

  // If no arguments passed, show usage
  if prompt == false && (name == "" || greeting == "") {
          flag.Usage()
          os.Exit(1)
  }

  // remainder omitted
}
Notice that we're utilizing AND operation (the && operator) because we want all of the conditions to be true for us to fall into the if statement's logic. For our code to work, we need either prompt to be present OR both name and greeting to be set. We've grouped name and greeting into a compound conditional because we should fail if either of them is empty and the OR operation (the || operator) will return true if either of the comparisons is true. It's worth noting that you won't find flag.Usage as a function in the documentation because it is a variable that the package exports. They do this so that we can override it ourselves, but we don't need to in our case. Environment Variables Before we adjust the actual logic of our program, we're going to add the ability to debug using an environment variable. While we're developing (or just using our tool) we might want to make sure that the flags are set properly by simply printing them out and we can achieve this by using an environment variable as a hidden flag. If the DEBUG environment variable is set to anything, we're going to simply print out the flags and cancel execution. Reading environment variables is yet another feature provided by the os package in the form of the os.Getenv function. Let's use this now:
func main() {
  // Previous omitted

  // Parse flags
  flag.Parse()

  // Optionally print flags and exit if DEBUG is set
  if os.Getenv("DEBUG") != "" {
    fmt.Println("Name:", name)
    fmt.Println("Greeting:", greeting)
    fmt.Println("Prompt:", prompt)
    fmt.Println("Preview:", preview)

    os.Exit(0)
  }

  // Remaining omitted
}
The os.Getenv function will always return a string and, if it is anything other than the empty string, we'll show our flag values. Now we can test our CLI without actually modifying anything:
$ go run main.go -greeting "Hello"
Usage of /tmp/go-build960899595/b001/exe/main:
  -greeting string
        greeting to use within the message
  -name string
        name to use within the message
  -preview
        use preview to output message without writing to /etc/motd
  -prompt
        use prompt to input name and message
exit status 1
Notice that the name of the program is based on the executable name. Since we used go run, it creates a temporary file and uses that as the name. Let's run with some flags now:
$ DEBUG=true go run main.go -greeting "Hello" -name "Keith"
Name: Keith
Greeting: Hello
Prompt: false
Preview: false
Optionally Reading From Stdin Continuing with our roadmap, we're ready to implement the optional Stdin parsing. We're going to extract the code that we currently have creating the bufio.Reader and prompting the user so that it is easy to call from within a conditional in the main function. Let's call this renderPrompt:
func renderPrompt() (name, greeting string) {
  reader := bufio.NewReader(os.Stdin)

  fmt.Print("Your Greeting: ")
  greeting, _ = reader.ReadString('n')
  greeting = strings.TrimSpace(greeting)

  fmt.Print("Your Name: ")
  name, _ = reader.ReadString('n')
  name = strings.TrimSpace(name)

  return
}
Notice that this function is returning two strings that we're calling name and greeting. Now we can use this function and set new values for the name and greeting variables that we have within the main function like this:
func main() {
  // previous omitted

  // Optionally print flags and exit if DEBUG is set
  if os.Getenv("DEBUG") != "" {
    fmt.Println("Name:", name)
    fmt.Println("Greeting:", greeting)
    fmt.Println("Prompt:", prompt)
    os.Exit(0)
  }

  // Conditionally read from stdin
  if prompt {
    name, greeting = renderPrompt()
  }

  // Generate message
  m := message.Greeting(name, greeting)

  // remainder omitted
}
Now we're using name and greeting in our call to message.Greeting to generate our message. Previewing Message Our last step is to preview our message instead of writing to the actual file. We can do this with another conditional based on the preview variable. Here's what the remainder of our function looks like:
func main() {
  // previous omitted

  // Generate message
  m := message.Greeting(name, greeting)

  // Either preview message or write to file
  if preview {
    fmt.Println(m)
  } else {
    // Write content
    f, err := os.OpenFile("/etc/motd", os.O_WRONLY, 0644)

    if err != nil {
      fmt.Println("Error: Unable to open to /etc/motd")
      os.Exit(1)
    }

    defer f.Close()

    _, err = f.Write([]byte(m))

    if err != nil {
      fmt.Println("Error: Failed to write to /etc/motd")
      os.Exit(1)
    }
  }
}

Hands-on Labs are real live environments that put you in a real scenario to practice what you have learned without any other extra charge or account to manage.

02:00:00

Third-Party Packages

Working with Third-Party Packages

Downloading and Installing Packages

00:04:24

Lesson Description:

There are a lot of powerful programs that we can build using only the standard library, but Go also has a robust open source ecosystem. In this lesson, we'll take a look at how we can download and use open source Go tools and libraries. Documentation For This VideoGo Command Documentation: Downloading and Installing PackagesDiagram Downloading Go Packages We've written a functional CLI using Go and the standard library, but now we're going to see how we could have written it using a popular CLI creation library called Cobra. If you've ever used Docker or Kubernetes then you've already interacted with CLIs that use this library. One of the benefits of relying on OSS is that we can leverage the communitie's skills and efforts so that we don't have to waste our time reinventing the wheel. Before we can start using Cobra, we'll need to download the source code for it. Thankfully, the go command has a subcommand that allows us to download a package simply by using the repositories URL. Let's use the go get command to download Cobra:

$ go get github.com/spf13/cobra
For this command, no news is good news and we can expect that the code was downloaded. The package will be located within $GOPATH/src/github.com/spf13/cobra. By using a URL, Go packages are effectively namespaced to the author or organization and makes it easy to see which package we've imported into our code, if multiple users provide a package with the same name. If a Go package defines a main package with a main function then it also provides an executable. Cobra provides it's own CLI for generating CLI projects, but it is within a nested package. If we run go get again with the full path to the package with the executable it will also install it for us:
$ go get -u github.com/spf13/cobra/cobra
Now, we have access to the cobra executable. Installing Go Based Tools If you've taken a look at the go command and its subcommands, you may have noticed that there is a go install command. This doesn't work exactly how you might think. The go get tool effectively checks if the package needs to be downloaded, downloads it, compiles it, and installs it by placing any executables in $GOPATH/bin. The go install tool does the same thing, except it won't download the package, so it needs to be on the machine already. We'll use go install when we want to have our motd executable built and put into $GOPATH/bin.

Revamping a CLI Using the Cobra Package

00:18:04

Lesson Description:

Now that we've downloaded Cobra, we're ready to use it to make a more robust version of our message of the day tool. In this lesson, we'll create a tool that is functionally equivalent to what we have while relying on some third-party code. Documentation For This Video Cobra Github PageCobra Generator READMEEffective Go: The init FunctionDiagram Generating a New CLI We're not going to remove our existing project. Instead, we're going to create a new package to hold the Cobra version of our tool. For this package, we're going to use cobra init to generate the project and we'll use a URL for the package with our Github usernames (substitute your own for [USERNAME]):

$ cd $GOPATH/src/github.com
$ mkdir [USERNAME]
$ cobra init --pkg-name github.com/[USERNAME]/motd [USERNAME]/motd
Your Cobra application is ready at
/home/cloud_user/go/src/github.com/[USERNAME]/motd

Give it a try by going there and running `go run main.go`.
Add commands to it by running `cobra add [cmdname]`.
$ cd [USERNAME]/motd
By default, we have the following files: main.go: The entry point for our CLI.cmd/root.go: The root command. We can add other commands within the cmd directory/package.LICENSE: The default license for the project is the Apache v2 license, but we can remove or change this file if we'd like to. Taking a look at the main.go file, we'll see that it calls the Execute function exposed by our cmd package:
package main

import "github.com/linuxacademy/motd/cmd"

func main() {
  cmd.Execute()
}
The Root Command The bulk of the work that we'll be doing will be in the cmd/root.go file because we don't have any subcommands. We now have the opportunity to see how flags are defined in Cobra. The generator actually added a lot of information, but looking through the file, the first thing that we'll notice is that the command itself is a pointer to a cobra.Command struct:
package cmd

import (
  "fmt"
  "os"

  homedir "github.com/mitchellh/go-homedir"
  "github.com/spf13/cobra"
  "github.com/spf13/viper"
)

var cfgFile string

// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
  Use:   "motd",
  Short: "A brief description of your application",
  Long: `A longer description that spans multiple lines and likely contains
examples and usage of using your application. For example:

Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,
  // Uncomment the following line if your bare application
  // has an action associated with it:
  Run: func(cmd *cobra.Command, args []string) {
  },
}

// remainder omitted
Notice that this is a variable defined outside of a function. Up until now, we've been working entirely inside the context of our main function, but a package can define variables, constants (defined like variables except using the const keyword and can't be modified later), functions, structs, and interfaces. Any of these items can also be exported by being capitalized. This is how we have access to the cobra.Command struct. When the struct is created, we're setting its attributes and you'll notice that it works a lot like setting keys in a map. Each of these attributes has a type and we must provide a value of that type (or not set the attribute). The Use, Short, and Long attributes work as documentation, and the Run attribute is actually a function that specifies what the command does. The Execute Function The Execute function that is defined isn't that complicated but it does show us how to use a compound statement in a conditional:
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
  if err := rootCmd.Execute(); err != nil {
    fmt.Println(err)
    os.Exit(1)
  }
}
By putting a semi-colon (;) after the err := rootCmd.Execute() expression, we can have more than one thing occur before evaluating the conditional. In this case, if the command errors when running its Execute method, then the nil check will return true and we'll print an error before exiting with a non-zero status code. The init Function The main function has been the primary special function that we've talked about, but there is actually another one. The init function can be defined by every source file and it runs to set up the state that the file needs. This function will run after variables and constants are initialized. The init function is often used to set the values of variables local to the package before the code is used. By default, our init function is doing a few things: Running a different initialization function to load a configuration file.Creating an example "persistent" flag that can be accessed by the child commands of the rootCmd.Creating an example local flag that can only be used by the rootCmd. These are created examples of things that could be done in an init function. In our case, this points us in the direction that we need to go to define the flags for our tool. Defining Flags Since we only have one command, we'll be defining local flags within our init function. Let's follow the example set by the generated code to add our greeting, prompt, name, and preview flags:
package cmd

import (
  "fmt"
  "os"

  "github.com/spf13/cobra"
)

var name string
var greeting string
var preview bool
var prompt bool
var debug bool = false

// rootCmd and Execute omitted

func init() {
  rootCmd.Flags().StringVarP(&name, "name", "n", "", "Name to use in message")
  rootCmd.Flags().StringVarP(&greeting, "greeting", "g", "", "Greeting to use in message")
  rootCmd.Flags().BoolVarP(&preview, "preview", "v", false, "Preview message instead of writing to /etc/motd")
  rootCmd.Flags().BoolVarP(&prompt, "prompt", "p", false, "Prompt for name and greeting")

  if os.Getenv('DEBUG') != "" {
    debug = true
  }
}
We're now declaring our variables at the top of our file and they'll be accessible by any function in the file. Notice that the flags are now tied to the rootCmd itself and calling rootCmd.Flags() returns the flag set for the command. The StringVarP and BoolVarP methods that we're calling are slightly different from what we used from the flag package because we are also specifying the variable shorthand as the third argument. We're also going to use a variable for debug and we can determine that in our init function. The Run Function With all of our flags and variables set, we're now ready to uncomment the Run attribute of our rootCmd and implement the function. This implementation will be pretty close to what we had before except that we no longer need to worry about setting up the flags within the function itself. We're going to copy the Greeting and renderPrompt functions from our previous implementation (renaming Greeting to buildMessage):
package cmd

import (
  "bufio"
  "fmt"
  "os"
  "strings"

  "github.com/spf13/cobra"
)

// variable declaration omitted

// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
  Use:   "motd",
  Short: "A utility to customize the Message of the Day (MOTD)",
  Long: ``,
  Run: func(cmd *cobra.Command, args []string) {
    // If no arguments passed, show usage
    if !prompt && (name == "" || greeting == "") {
      cmd.Usage()
      os.Exit(1)
    }

    // Optionally print flags and exit if DEBUG is set
    if debug {
      fmt.Println("Name:", name)
      fmt.Println("Greeting:", greeting)
      fmt.Println("Prompt:", prompt)
      os.Exit(0)
    }

    // Conditionally read from stdin
    if prompt {
      name, greeting = renderPrompt()
    }

    // Generate message
    m := buildMessage(name, greeting)

    // Either preview message or write to file
    if preview {
      fmt.Println(m)
    } else {
      // Write content
      f, err := os.OpenFile("/etc/motd", os.O_WRONLY, 0644)

      if err != nil {
        fmt.Println("Error: Unable to open to /etc/motd")
        os.Exit(1)
      }

      defer f.Close()

      _, err = f.Write([]byte(m))

      if err != nil {
        fmt.Println("Error: Failed to write to /etc/motd")
        os.Exit(1)
      }
    }
  },
}

// remainder of file omitted
func buildMessage(name, greeting string) string {
        return fmt.Sprintf("%s, %s", greeting, name)
}

func renderPrompt() (name, greeting string) {
        reader := bufio.NewReader(os.Stdin)

        fmt.Print("Your Greeting: ")
        greeting, _ = reader.ReadString('n')
        greeting = strings.TrimSpace(greeting)

        fmt.Print("Your Name: ")
        name, _ = reader.ReadString('n')
        name = strings.TrimSpace(name)

        return
}
The main change that we had to make was that we were no longer using the flag package and to print the usage information we had to use the cmd.Usage() method. The help/usage text produced by Cobra is great and this is the method that we'll use to display it (specific to the command being run).

Distributing Go Programs

Distributing Go

Compiling a Cross-Platform Go Application

00:03:30

Lesson Description:

Now that we've written a full-featured application, we're ready to build it to release to our coworkers. In this lesson, we'll take a look at how Go enables us to build our applications to run on all major operating systems. Documentation For This VideoThe go Command Environment VariableAvailable Options for GOOS and GOARCHDiagram Using go build We're ready to build our motd program so that we can share it with others. Go has great support for compiling for various operating systems and CPU architectures. The go build tool is what we need to compile our binary, and then we need to provide different values for the GOOS and GOARCH environment variables before we run the command. Here's what each of those environment variables does:GOOS: The operating system to compile the code for; examples being linux, windows, freebsd, darwin, netbsd, etc.GOARCH: The CPU architecture to compile the code for; examples being amd64, 386, arm, and arm64. Our workstation is running linux and uses the amd64 architecture, so we will use those values for GOOS and GOARCH by default. From within our $GOPATH/src/github.com/keiththomps/motd directory, let's run two go build commands to create a binary for Linux and also 64-bit macOS:

$ go build
$ GOOS=darwin GOARCH=amd64 go build -o motd.darwin
$ ls
cmd  LICENSE  main.go  motd  motd.darwin
Now we have two binaries, one that will run on Linux and another that will run on a macOS system.

Hands-on Labs are real live environments that put you in a real scenario to practice what you have learned without any other extra charge or account to manage.

00:30:00

Conclusion

Final Steps

What's Next?

00:01:12

Lesson Description:

Thank you for taking the time to go through this course! I hope that you learned a lot and I want to hear about it. If you could please take a moment to rate the course, it will help me know what is working and what is not. Now that you have a grasp of Go, there are a lot of doors open to you to continue your learning. You can start digging into writing web applications with Go, contributing to popular tools like Docker or Kubernetes, or just making more and more sophisticated tools for you and your colleagues.