Guide to Golang

This is a quick whirlwind tour of the Go programming language! Please feel free to use this as reference for your assignments. We highly recommend starting with A Tour of Go to get through the basics quickly and interactively; however, this guide will seek to be as comprehensive as possible. For other resources to learn Go, check out:


Setup

To get started with Go, first ensure that you have the Go programming language installed by running go --version in the terminal. If you don't have Go installed, follow the installation instructions here. Note that Go 1.13 is already installed on all department machines. Please install Go 1.13 or above.

To set up a Go project, navigate to your project folder and run go mod init <mod_name>. The module name is generally determined by your GitHub setup. If your GitHub username is jcarberry and your repository name is db, you should run go mod init github.com/jcarberry/db. This creates a Go module, which is essentially a virtual environment complete with a package system. Most Go projects should be in a Go module.

You can import external packages into your module for use in your code. Running go get <pkg> will add the package to your go.mod file and create a checksum file, go.sum. Then, the package will then be accessible to your code. Try it out on github.com/couchbase/vellum!

Some commands that are useful:


Building and Running

Now that you've installed and initialized a Go project, it's time to learn how to run it!

We'll explain what a main package is in the next section. In this class, building and running is handled by our Makefile. We typically build the package into an executable to avoid creating too many binaries in your $PATH.


Project Structure

The basic structure of every Go project is about the same. In your root directory will be your go.mod file alongside any other configuration files or build scripts, like a Makefile or README. There will be two or three top-level directories: cmd and pkg, and potentially internal.

cmd is where the primary entrypoint(s) to your project will live, separated into subfolders that contain a main package. Each main package is an entrypoint to your application and can be compiled into an executable. Let's say you had a file: cmd/db/main.go - running go build ./cmd/db would create an executable ./db that would run the code inside the associated main.go file. Similarly, go run ./cmd/db/main.go would run the package. ·

pkg is where your project's packages live. A package is a collection of related Go files, typically implementing a single piece of logic. To use a package in your binaries, you have to import them using the module name you defined above (more on this later). Packages are self-contained, and everything capitalized will be exported from the package (e.g. func Sample() is exported, func secret() is not). The main package is specially designed to be compiled into a binary. You can have multiple main packages, each in a subfolder in the cmd directory (detailed above).

internal is where private packages live; privacy is guaranteed by the compiler, and code here can't be imported outside of the module. In this course, we don't use the internal folder.


Hello World

The following is a classic "Hello World" program:

package main

import "fmt"

func main() {
	fmt.Println("Hello, world!")
}

The first line of every Go file must be the package it belongs in; in this case, main. Next are the packages that are imported using the import keyword. You can (and should) import multiple packages, both from the standard library and from external sources using the following syntax:

import (
    "fmt"
    "os"
    
    db "github.com/jcarberry/db/pkg"
)

You will have to import packages from your own module into other parts of your module. However, all code in a module is accessible to all other files in that module, even unexported values.


Printing

The fmt package is the main package for printing content out to the terminal:

fmt.Println("Three, Two, One")
fmt.Printf("%d, %d, %d\n", 3, 2, 1)

Types

Go's basic types are:

bool
string
int  int8  int16  int32  int64
uint uint8 uint16 uint32 uint64 uintptr
byte // alias for uint8
rune // alias for int32, represents a Unicode code point
float32 float64
complex64 complex128

and their pointer variants, which are prefixed with a *. Each type has a zero value, which is 0 for numeric types, false for boolean types, and "" for strings.

You can convert between types using Type(v). Be aware of how the representation of data may affect the underlying data, however.


Variables

Variables can be declared using the var keyword or using the := operator:

var x int
x = 10
var y = 11  // Types are inferred.
z := 12     // Types are inferred.
a, b := 13, 14 // Can initialize two at a time!

Note that the := operator is not available outside of functions.

There is also a const keyword that can be used to declare constants outside of a function. By convention, constants should be named using SCREAMING_SNAKE_CASE.


Functions

Functions can be declared like so:

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

Use the keyword func, give the function a name, and type each of the parameters and outputs. Functions can have multiple outputs and named outputs:

func split(sum int) (x, y int) {
	x = sum * 4 / 9
	y = sum - x
	return
}

Calling functions is rather straightforward as well:

fmt.Println(add(10, 11)) // Prints 21

You can also define functions anonymously and inline:

isEven := func(x int) bool { return x % 2 == 0 }

Loops

Go loops are declared using the for keyword. There are two main ways to write a for loop, traditionally and using the range keyword:

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

primes := [6]int{2, 3, 5, 7, 11, 13}
for idx, val := range primes {
    fmt.Println(idx, val)
}

While loops are written as a for loop with no condition:

for {
    fmt.Println("ever")
}

Control Flow

if statements are written as follows:

x := 10
if x < 8 {
    return 1
} else if x > 10 {
    return 2
} else {
    return 3
}

You can declare variables to be used in the if statement, but variables declared like this will be scoped to the if block, and be inaccessible outside of it:

if v := f(); v > 10 {
    return true
}
fmt.Println(v) // will error

To check a number of cases, use the switch statement:

switch v {
case 10:
    return false
case 12:
    return true
default:
    return false
}

To have a function be invoked only when the calling function returns, use the defer keyword:

func sum() {
    defer fmt.Println("Yoohoo")
    fmt.Println("Yahoo")
    // Prints "Yahoo" then "Yoohoo"
}

Pointers

Go has pointers that hold memory addresses. If you have never worked with pointers before, we recommend reading up about C pointers to get an idea of how they work (Go pointers are similar to C pointers, just without pointer arithmetic). The zero value of a pointer is nil. Define and dereference a pointer like so:

x := 10      // x holds 10
ptr := &x    // ptr holds a reference to x
val := *ptr    // val holds the value of x

You'll often see constructors that create pointers:

func NewDog() *Dog {
    return &Dog{paws: 4, rating: 10}
}

Arrays, Slices, and Maps

Arrays are fixed-size composite data types. The type [n]T is an array of n values of type T. Declare an array, filled with its zero value or initialized yourself, like so:

var names [2]string
primes := [6]int{2, 3, 5, 7, 11, 13}

Slices are dynamically-sized views of an array; they are much more commonly used than arrays. The type []T is a slice of values of type T. While there are many ways to define a slice, the most useful one uses the make function, while using the append function to add more elements to the slice:

names := make([]string, 0)
names = append(names, "sparky")

Use the len function to find the lengths of slices (and many other datatypes).

names := make([]string, 8)
length := len(names) // 8

Maps are key-value stores like Python dictionaries or Javascript objects. Declare and use a map like so:

m = make(map[string]int)
m["ten"] = 10
fmt.Println(m["ten"])

ten := m["ten"]
fmt.Println(ten)

delete(m, "ten")
fmt.Println(m["ten"]) // errors

ten_check, ok := m["ten"]
fmt.Println(ok) // false
fmt.Println(ten_check) // 0

Structs

A struct is a collection of fields:

struct Dog {
    name string
    legs int
}

To initialize and print a struct, see the following example:

sparky := Dog{name: "sparky", legs: 4}
fmt.Printf("%+v \n", sparky)

You can creates pointers to structs and access their fields in the exact same way (automatic dereferencing):

sparky := Dog{name: "sparky", legs: 4}
ptr := &sparky
fmt.Println(ptr.name)

You can define methods on structs (functions with a struct as a receiver) like so:

func (d Dog) bark() {
    fmt.Println("Bark")
}

sparky := Dog{name: "sparky", legs: 4}
sparky.bark() // prints "Bark"

Only pointer methods can mutate a struct:

// Won't do anything
func (d Dog) growLeg() {
    d.legs += 1
}

// Euruka!
func (d *Dog) growLeg() {
    d.legs += 1
}

Interfaces

An interface is a set of method signatures. There is no implements keyword; any struct that has a method for every method signature automatically implements the interface.

type Animal interface {
    walk()
    name() string
}

The empty interface interface{} is implemented by every type, and is useful for when a type is unknown. We can cast from an interface{} type to another type (unsafely) using the i.(type) syntax:

anyMap := make(map[string]interface{}) // Can put anything in this map
anyMap["one"] = 1
anyMap["two"] = "two"
anyMap["three"] = Number{value: 3}

one := anyMap["one"].(int)
one := anyMap["one"].(string) // This will panic!

One thing to note about interfaces is that an interface can be implemented by either a struct a pointer to that struct; as a result, you should almost never use a pointer to an interface in a function header, as it can cause some confusion:

func Wrong(i *SomeInterface) {} // This will not work as expected, even if your struct has pointer receiver methods

Errors

The error type is used to express errors. It is often returned by functions to signal whether or not the function ran as expected. The following is a very common pattern in Go:

func mightFail(input int) (int, error) {
    if input == 0 {
        return -1, errors.New("Can't use 0")
    } else {
        return 10 / input
    }
}

func main() {
    result, err := mightFail(0)
    if err != nil {
        return err
    }
    fmt.Println(result)
}

The errors package is useful for creating errors.


Concurrency

This section will be populated later in the course. Important topics to review include:


Testing and Benchmarking

Unit tests in Go are written in files that end in _test.go. Typically, unit tests for a given package live in the same folder as the package itself. Unit tests are simply functions that begin with Test and take one parameter of type *testing.T. You can run all of the tests for a given package using go test [-v]. The following is an example test:

func TestAdd(t *testing.T) {
    a, b := 10, 11
    if a + b != 21 {
        t.Error("Addition is broken")
    }
}

Benchmarks in Go are the exact same as tests, except that they must be prefixed with Benchmark and take one parameter of type *testing.B. To run benchmarks, run go test -bench=.. Benchmark code must be run multiple times; be sure to wrap your benchmarked code in a for loop that runs at most b.N times. To have control over the benchmarking timer, for instance, to allow for setup or teardown, use b.ResetTimer, b.StopTimer, and b.StartTimer. To initiate cleanup, use b.CleanUp.

To check your test's code coverage, run go test -coverprofile=.... In general, strive to cover most of your code with tests, about 80%. Note that high test coverage does not necessarily mean good testing, and good testing does not always result in high test coverage; given that this class also uses system testing, it may be that the majority of your testing infrastructure lies there. That is perfectly fine. What's important is that you are confident that your software works as intended.


Style & Other Tips