Go is ten years old and still going strong. It's the third-most wanted programming language after Python and JavaScript. Big projects such as Kubernetes, Docker, and Terraform use it, and developers love it, too.

If you always wanted to learn Go, this is the course for you. I intend to walk you through Go through multiple blog posts until you can write a complete REST API using containers and a CI/CD pipeline.

This first part of the course will help you develop your first Go application and introduce you to the language's basic concepts. Let's Go.

Step 1: Setting up your environment

First things first:

Once that's done, let's start coding.

Step 2: Understanding Go basics

Your First Go Application

What kind of course would this be if we wouldn't start with the classic "Hello World!" application?

Create a folder named hello-go and open Visual Studio Code on it. If you want to do this in a fancy way and familiarize yourself with the console/terminal in the process, just open your favorite terminal app and type:

mkdir hello-go && code $_

This will create a folder named hello-go in the current directory and open Visual Studio Code there. The $_ will get the arguments passed from the previous command, in this case hello-go.

So you're probably on the VS Code screen right now. Let's create a new file named main.go.  There are a few ways you can create a new file, but let's keep things simple and right-click in the folder area to choose New File, like so:

visual studio code new file screen
Creating a new file

Go programs are made with packages and the main package is the entry point of a Go program. Packages can be imported and, by convention, a package's name is the same as the last element of the import path.

Standard library packages can be called fmt or net/http, while the import path of other types of package is usually the repository where the package is held, such as https://github.com/gorilla/mux.

We want to be able to easily handle all our dependencies, so let's make use of go modules. Open VS Code's terminal by going to View > Terminal and type in:

go mod init github.com/<your-github-handle>/<your-go-repository-name>

If you don't have a GitHub account yet, I highly recommend you create one, so you can replace the placeholders in the above command with your GitHub handle and a repo name.

Now you have a go.mod file. This file holds your module's name, the Go version used to create it, and the external dependencies it has - with their respective versions. For now, the file has no dependencies, but that's about to change soon.

Let's get back to the main.go file. Write the following code in the file:

package main

import(
    "fmt"
)

func main(){
    fmt.Print("Hello World!")
}

When you save this file, VS Code will automatically format your code according to Go's format guidelines. Let's break down this code to understand what's going on:

The first line is the package declaration, which is composed of the keyword package and followed by the package's name.

Then we have our imports. For this specific piece of code, we're just importing the fmt package. This package is a toolbelt used to format the data of the standard output.

Next is our first function. Go function declarations are created using the keyword func, an optional binding, an optional function name, the function parameters, and the function's body. Our function is called main (following convention) and it simply prints "Hello World!" to the standard output.

To run this program, go to VS Code's terminal and type:

go run main.go

You should see "Hello World!" in the terminal. That wasn't too hard, was it?!

Private/Public Names

Go handles private/public names by convention. Anything starting with a capital letter - except for package names - is public while anything starting with a lower-case letter is private. When importing a package, only the public parts of it are accessible.

Variables

Variables can be created in a few ways, and they can be initialized or not. If a variable is not initialized, it takes the type's zero value. Let's use the var keyword to declare one or many variables. This can be done on the package or function level:

var (
	index int //int zero-value is 0
	noWord string //string zero-value is ""
	didWeLearnSomething bool = true //bool zero-value is false, but we're initializing the variable with true instead
)

Let's use the short variable declaration. This can only be done inside a function:

index := 3 //short variable declaration infers the type. In this case, index will be an int
noWord := ""
someWord := "bird"

Pointers

Pointers hold addresses to values. This is called "referencing a value". The zero value of a pointer is nil.

var index *int
fmt.Println(index)  //the zero value for pointers is nil, so that's what will be printed out
i := 4              //we assign 4 to variable i
index = &i          //the & token grabs the address of a variable, in this case we're assigning the address of i to index
fmt.Println(index)  //this will print out the memory address
fmt.Println(&i)     //checking that's the same address of i
fmt.Println(*index) //the * token is used for both declaring a pointer and for dereferencing a variable, getting its value
i = 42              //this will change index's value as well, since they point to the same memory address
fmt.Println(*index) //checking that index now holds 42

Structs

A struct is a collection of fields (private or public).

type Person struct {
	ID int
	Name string
	Phone string
}

Structs can be "instantiated" using literals, like this:

person := Person{
	ID:    1,
	Name:  "Cool Person",
	Phone: "+1 111 1111-1111",
}

fmt.Println(person.Name) //struct fields can be accessed through the dot syntax

Functions

A function can take zero or more arguments. It can return zero or more values as well. When a function fails for some reason, it's considered good practice to return a result and an error. Function callers can then check if the error is nil to understand if everything went as expected.

Named functions need to be declared outside of other functions, while anonymous functions can be declared inside functions.

package main

import (
	"errors"
	"fmt"
)

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

func processSomething(thing string) (string, error) {
	if thing == "Cool Thing" {
		return "processed successfully", nil
	}

	return "", errors.New("something bad happened")
}

func main() {
	greet := func(name string) {
		fmt.Println("Hello " + name)
	}

	greet("Cool Person")
	result := add(3, 4)
	fmt.Println(result)

	processingResult, err := processSomething("Not a Cool Thing")
	if err != nil {
		fmt.Println(err) //Go conditionals don't need to have parentheses
	} else {
		fmt.Println(processingResult)
	}
}

Functions can also be bound to Structs. This allows the function to have access to the function's private fields and act on its values.

package main

import (
	"fmt"
)

type Person struct {
	ID    int
	Name  string
	Phone string
}

func (person *Person) SayIntroduction() {
	fmt.Println("Hello! My name is " + person.Name)
}

func main() {
	person := Person{
		ID:    1,
		Name:  "Cool Person",
		Phone: "+1 111 1111-1111",
	}

	person.SayIntroduction()
}

Loops

Go only has one loop construct: for. The standard for loop consists of four components: the init statement, the condition expression, the statement that'll be executed after each iteration, and the loop body. Here's an example:

package main

import (
	"fmt"
)

func main() {
	for i := 0; i <= 10; i++ { //for i starting at zero, while i is less than or equal 10, increment i by one each iteraction
		fmt.Println(i)
	}
}

If you want a good old while loop, just drop the init statement and the post statement, like this:

package main

import (
	"fmt"
)

func main() {
	i := 0
	for i <= 10 { //for i starting at zero, while i is less than or equal 10, increment i by one each iteraction
		fmt.Println(i)
		i++
	}
}

Can you guess how to do an infinite loop?

Arrays and Slices

An array is a data structure capable of storing ordered data that's accessible with indexes. In Go, arrays can be declared like so:

fiveWords := [5]string{
	"zero",
	"one",
	"two",
	"three",
	"four",
}
fmt.Println(fiveWords)

This declares an array of five strings. An array can't be resized, but Go offers a few ways to work around that. To access the third value of the array, you could do the following:

thirdWord := fiveWords[2]
fmt.Println(thirdWord)

This grabs the value at index 2 and assigns it to the variable thirdWord. Why index 2 if we wanted the third value? Because indexes start at zero 🙂

Slices, on the other hand, reference underlying arrays. This means that they can be resized - by pointing to a bigger array. If we wanted just the first two words of our fiveWords array, we could slice it like this:

firstTwo := fiveWords[0:2]
fmt.Println(firstTwo)

This notation means "slice the array starting and including index 0 and finishing and excluding index 2." So we'll only get the values on indexes 0 and 1.

If we want to create a slice from scratch - instead of creating an array then slicing it - we could just omit the length:

myOddsSlice := []int{1, 3, 5, 7, 9}
fmt.Println(myOddsSlice)

And if we want to add items to the slice, we can use the append function:

myOddsSlice = append(myOddsSlice, 11, 13)
fmt.Println(myOddsSlice)

The append function takes a slice and any number of arguments to add to the slice. The arguments must be of the same type as the slice values.

Maps

Maps can be used whenever you want to pair a specific key with a value. Map declarations have their key and value types specified:

numbers := map[string]int{}
numbers["one"] = 1
numbers["two"] = 2
numbers["three"] = 3
fmt.Println(numbers)

If you check the printed result, you might notice that the map is not ordered like the array. Never take a map's standard ordering as a given. Instead, always pick the values by the keys.

Range

The range iterator can be used to traverse arrays, slices, and maps. When used on arrays and slices, it returns the index and the value of each iteration. When used on maps, it returns the key and the value of each iteration.

fiveWords := [5]string{
	"zero",
	"one",
	"two",
	"three",
	"four",
}

for index, value  := range fiveWords {
	message := fmt.Sprintf("index: %d - value: %s", index, value) //Sprintf takes a string template and values to interpolate
	fmt.Println(message)
}

numbers := map[string]int{}
numbers["one"] = 1
numbers["two"] = 2
numbers["three"] = 3

for key, value  := range numbers {
	message := fmt.Sprintf("key: %s - value: %d", key, value)
	fmt.Println(message)
}

This was the first part of the Go Crash Course. We've written our first Go application and familiarized ourselves with important aspects of the Go language. The second part of our crash course will guide you through a real-world scenario in Go.