paulgorman.org/technical

Go (golang)

(Created July 2016, updated 2020.)

Learn Go

  1. Install Go.
  2. Take a tour of Go.
  3. Read Donovan and Kernighan’s book The Go Programming Lanuage.

Update Go

Download a binary release from https://golang.org/dl/, then:

$ sudo rm -r /usr/local/go/
$ sudo tar -C /usr/local -xzf ~/Downloads/go1.14.6.linux-amd64.tar.gz

Hello, world

$ mkdir -p $GOPATH/src/example.com/me/hello
$ cd $GOPATH/src/example.com/me/hello
$ vi hello.go
package main

import (
	"fmt"
)

func main() {
	fmt.Println("Hello, world!")
}
$ go vet
$ go fmt
$ go run
Hello, world!
$ go build
$ ./hello
Hello, world!
$ go install
$ $GOPATH/bin/hello
Hello, world!

Documentation

https://golang.org/doc/

$ go doc fmt | grep 'uint'
	uint, uint8 etc.:        %d, %#x if printed with %#v
$ go doc fmt Println
$ go doc -src fmt Println

Show the docs for the package in the current directory:

$ go doc .

Comments (and go doc Commentary)

Go’s go doc tool generates documentation from comments.

// Copyright 2020 Paul Gorman. All rights reserved.
// The MIT license governs use of this software; see the LICENSE file.

// Package hello provides a friendly greeting.
// (A comment at the head of the package gets used by 'go doc' to auto-generate docs.)
package hello

// Hello returns a greeting string.
// (A comment proceeding a function declaration gets used by 'go doc' to auto-generate docs.)
func Hello() string {
	return "Hello, world!"
}

// Double front-slashes denote a comment that runs to the end of the line.

/* A slash
   and a splat
   starts a multi-line comment that ends with
   a splat and a slash.
*/

Types

Variables

// Declare variables then assign values
var x int
x = 100
// …or declare and assign in one statement:
var y int = 10

// Using the `:=` operator in a declaration infers type:
x := 100

// Subsequent assignments to already-declared `x` must use the `=` operator:
x = 2

// For multiple variables, as long as one is newly declared, `:=` works:
x, y, z := 9, "UFO", 42

// A constant:
const pi float32 = 3.14159

For

// `for` is Go's only loop.
for i := 0; i < 10; i++ {
	sum += i
}

// Omitting the loop's initialization and post-iteration statements makes a `while` loop:
sum := 1
for sum < 1000 {
	sum += sum
}

// Loop forever by omitting even the loop's test statement:
for {
	foo()
}

// Iterate over a slice:
sl := []string{"a", "b", "c", "d"}
for index, value := range sl {
	fmt.Println(index, " : ", value)
}

// Note that `range` gives a _copy_ of the value.
// Changes made to the copy don't affect the original value outside the loop.
// To change the original, reference it by index, like origSlice[i].
sl := []string{"a", "b", "c", "d"}
for index, value := range sl {
	value = "x" // `sl` is unchanged.
	sl[index] = "x" // `sl` changed.
}

If

// `if` can start with multiple short statements.
// `x` is scopped to the `if` block.
if x := blerg(); x < 0 {
	return x + 1
} else if y == 99 {
	return y
} else {
	return x - 1
}

Switch

Each case in a switch breaks implicitly (unless it ends with fallthrough).

Like if, ‘switch’ can have multiple short preliminary statements.

fmt.Print("Go runs on ")
switch os := runtime.GOOS; os {
case "darwin":
	fmt.Println("macOS.")
case "linux":
	fmt.Println("Linux")
default:
	fmt.Printf("%s.", os)
}

Without a condition, switch acts like switch true. This can be used for long if-then-else chains.

Functions

// Functions can return multiple values:
func vaporize(name string) (int, bool) {
	// …
}

// Discard uninteresting return values by assigning them to the magic underscore:
_, b = vaporize("bar")

// Go has "function literals" (a.k.a. anonymous functions).
// Function literals are closures.
// They share variables defined in their surrounding function.
// Those shared variables survive as long as they are accessible.
f := func(x, y int) int { return x + y }

// Define new function types:
type Add func(a int, b int) int

Defer

Go’s defer keyword delays execution of a function until return of the enclosing function. Defer mainly helps with “clean-up” tasks.

Arguments to deferred functions are evaluated immediately (not when the surrounding function returns).

Deferred function calls get pushed onto a stack. When a function returns, its deferred functions get called in last-in-first-out order.

func main() {
	file, err := os.Open("myfile")
	if err != nil {
		fmt.Println(err)
		return
	}
	defer file.Close()
	// … Read the file, etc.
}

Variadic Functions

A variadic function can be called with a varying number of arguments.

/// Prepend the final parameter type with an ellipsis:
func sum(vals ...int) int {
	total := 0
	for _, val := range vals {
		total += val
	}
	return total
}

sum(1, 2, 3, 4)

// Invoking a variadic function with a slice argument:
values := []int{1, 2, 3, 4}
sum(values...)

Strings

// Raw string literals are useful for defining regular expressions,
// or embedding templates in code, like a Perl heredoc.
x := `This is
a raw string
literal.`

// Convert a string to a byte array:
s := "Keep off the moors!"
bytearray := []byte(s)

// Convert a byte array to a string:
s2 := string(bytearray)

// The `+=` operator concatenates strings.
s += " bar"
// However, `+=` creates a new string.
// For large or repeated uses, use `Join` from the `strings` package:
strings.Join(os.Args[1:], " ")

From section “3.5.4. Strings and Byte Slices” of The Go Programming Language:

Because strings are immutable, building up strings incrementally can involve a lot of allocation and copying. In such cases, it’s more efficient to use the bytes.Buffer type[…]

new() and make()

Go had two allocation primitives: new and make. They work on different types, and do slightly different things.

new is a built-in function. It allocates memory. However, rather than initialize that memory, new only zeroes it. So, new(T) allocates and zeroes enough memory to hold a value of type T. new returns a pointer to this newly-allocated, zero-value T.

Where possible, design data structures usable with the zero-value of each type, without further initialization. For example, the zero-value of bytes.Buffer give an empty Buffer ready for use.

Unlike new, the built-in function make works only on slices, maps, and channels. Also unlike new, make(T, args) return an initialized (non-zeroed) value of type T. Go uses make for these types because slices, maps, and channels always require initialization before use.

var p *[]int = new([]int)       // This allocates a slice structure. Since `*p == nil`, this is rarely useful.
var v  []int = make([]int, 100) // The slice `v` now refers to a new array of 100 `int`s.

// Unnecessarily complex:
var p *[]int = new([]int)
*p = make([]int, 100, 100)

// Idiomatic:
v := make([]int, 100)

Pointers

Go passes by value.

When passing an argument to a function, Go creates a copy of the value that lives for the scope of the function. Changes made inside the function only affect the copy.

By passing a pointer to a function, changes to the value survive outside the function.

// `new` allocates memory and initializes it to a zero value.
x := new(int)

// `y` points to int-size memory, but the value there remains `nil`.
var y *int

fmt.Println(x) // 0xc420016100
fmt.Println(*x) // 0
fmt.Println(&y) // 0xc420084018
fmt.Println(y) // Nil

Is passing a pointer more efficient than copying a value?

Passing a pointer is probably more efficient for large structs, but pass simple scalar values (ints, slices, etc.) as copies. Passing simple values as pointers invites inefficiencies from cache misses and heap allocations.

https://www.reddit.com/r/golang/comments/a410gl/go_is_passbyvalue_does_that_mean_each_function/ebb12dw/

Stucts

type Car struct {
	Model string
	Wheels int
	Speed int
}

mycar := Car{
	Model: "Beetle",
	Wheels: 4, // Note *required* trailing comma!
}
secondcar := Car{}
secondcar.Wheels = 3

thirdcar := &Car{"Hexamobile", 6, 0} // Pointer to new Car.

Go structures can receive methods, making them a bit like objects.

func (c *Car) Accelerate() { // *Car is the receiver of the Accelerate() method.
	c.Speed += 10
}

thirdcar.Accelerate()

(Any Go type can receive methods.)

Go structs support embedded types. These are awkward to use in some ways, but let us call methods of the embedded type on the parent struct.

type Dunebuggy struct {
	Car
	Grooviness int
}

// …

b := Dunebuggy{
	Car: Car{Wheels: 4,},
	Grooviness: 19,
}
fmt.Println(b.Wheels)
b.Accelerate()
fmt.Println(b.Speed)

Anonymous structs:

car := struct {
	make   string
	model  string
	color  string
	weight float64
	speed  float64
}{
	"Ford",
	"Destroyer",
	"orange",
	3628.7,
	227.4,
}
fmt.Println(car.speed)

Use struct tags to appease JSON consumers that want lowercase key names:

type Animal struct {
	Species string `json:"species"`
	Color   string `json:"color"`
}

Composition

Instead of hierarchical inheritance, Go supports composition. Compose hybrid types by including one struct type in another. (Some languages call this a “mix-in” or “trait”.)

type Vehicle struct {
	Make string
	Model string
}

type Bicycle struct {
	*Vehicle
	Wheels int
	Riders int
}

func (b *Bicycle) String() string {
	return fmt.Sprintf(
		"Make: %s, Model: %s, Wheels: %d, Riders: %d",
		b.Vehicle.Make, b.Model,    // Go exposes both "b.Vehicle.Model" and the shorter "b.Model".
		b.Wheels, b.Riders,
	)
}

func main() {
	mybike := &Bicycle{
		Vehicle: &Vehicle{"Raleigh", "Tourist"},
		Wheels: 2,
		Riders: 1,
	}
	fmt.Println(mybike)
}

Overloading is the ability to create multiple methods with the same name differentiated either by type of argument or number of arguments. Go does not support overloading. However, we can overwrite the methods of a mixed-in type (and if necessary still call an overwritten method like thing.Mixin.foo()).

Factories not Constructors

Go doesn’t have constructors for objects, but the factory pattern helps create new structs.

type Car struct {
	Model string
	Wheels int
	Speed int
}

func NewCar(model string, wheels int, speed int) *Car {
	return &Car{    // We return a pointer to the new struct.
		Model: model,
		Wheels: wheels,
		Speed: speed,
	}
}

func main() {
	monsterTruck := NewCar("Crusher", 4, 1000000)
	fmt.Println(monsterTruck.Speed)
}

Default Function Arguments

https://talks.golang.org/2012/splash.article

One feature missing from Go is that it does not support default function arguments. This was a deliberate simplification. Experience tells us that defaulted arguments make it too easy to patch over API design flaws by adding more arguments, resulting in too many arguments with interactions that are difficult to disentangle or even understand. The lack of default arguments requires more functions or methods to be defined, as one function cannot hold the entire interface, but that leads to a clearer API that is easier to understand. Those functions all need separate names, too, which makes it clear which combinations exist, as well as encouraging more thought about naming, a critical aspect of clarity and readability.

Arrays

In most cases, use a slice instead of an array.

The size of an array never changes after creation.

var counts [10]int    // Create an array
counts[0] = 102    // Assign a value to an array.

weights := [3]int{3492, 487, 48762}    // Create and initialize an array

for index, value := range weights {    // Iterate over an array.
	// …
}

Slices

Slices are an abstraction on top of arrays, providing growable lists.

weights := []int{234, 3458435, 223, 23432}    // Create and populate a slice.
weights := make([]int, 10)    // Create a slice with length 10 and capacity 10 (values initialized to "0").
weights := make([]int, 0, 10)    // Create a slice with length 0 and capacity 10.

If we force Go to grow the capacity of a slice with append(), it automatically copies all the values into a larger array. Go uses a two-times algorithm — each time it grows the slice, the new array doubles the size of the old one.

a := make([]int, 5)
fmt.Println(len(a), cap(a))    // 5 5

a = append(a, 1)
fmt.Println(len(a), cap(a))    // 6 10

for i := 0; i < 5; i++ {
	a = append(a, 1)
}
fmt.Println(len(a), cap(a))    // 11 20

for i := 0; i < 10; i++ {
	a = append(a, 1)
}
fmt.Println(len(a), cap(a))    // 21 40

Go accepts slice notation, (although not negative notation, like x[:-2]).

x := []int{2, 4, 8, 12, 16, 20, 24}
fmt.Println(x[2:5])    // 8 12 16
fmt.Println(x[:3])    // 2 4 8
fmt.Println(x[5:])    // 20 24
y := make([]int, 3)
copy(y, x[3:6])    // Copy part of one slice to another.
fmt.Println(y)    // 12 16 20

The copy function creates entries in a destination slice to match the source slice, overwriting any existing destination entries:

s1 := []int{0, 1, 2, 3}
s2 := make([]int, 4)
copy(s2, s1) // Both slices now hold [0 1 2 3].

Maps

Maps hold keys and values (like dictionaries in Python). Initialize an unpopulated map with make, like:

m = make(map[string]int)

Maps grow dynamically, but make accepts an optional second argument for initial length. The delete function removes items from the map.

ages := make(map[string]int)
ages["Ben"] = 26
ages["Richard"] = 31
fmt.Println(ages["Ben"])
fmt.Println(len(ages))    // 2

delete(ages, "Ben")
fmt.Println(len(ages))    // 1

codenames := map[string]string {    // Another way to declare and initialize maps.
	"Ben": "Brown Boxer",
	"Richard": "Steam Shovel",
	"Phil": "Panther",
	"Anne": "Seatle",
}

for key, value := range codenames {
	fmt.Println(key, "\t:\t", value)
}

type Monstertruck struct {
	Name string
	Tirepressure map[string]int
}

mytruck := &Monstertruck {
	Name: "King Krusher",
	Tirepressure: map[string]int {
		"Front-left": 100,
		"Front-right": 99,
		"Rear-left": 100,
		"Rear-right": 97,
	},
}

fmt.Println(mytruck.Tirepressure["Rear-right"])

Writing to a nil map causes a runtime panic.

// A map retrieval yields a zero value when the key is not present:
if capitals["Michigan"] != "" {
	fmt.Println(capitals["Michigan"])
}

// …or, check for a second "ok" value to test the result of a map lookup:
if city, ok := capitals["Michigan"]; ok {
	fmt.Println(city, ok) // -> "Lansing" true
}

When iterating over a map with a range loop, the order is not guaranteed to be the same from one run to the next.

Maps are not safe for concurrent use. Protect maps with sync.RWMutex.

var counter = struct{
	sync.RWMutex
	m map[string]int
}{m: make(map[string]int)}

// Lock to to read:
counter.RLock()
n := counter.m["some_key"]
counter.RUnlock()
fmt.Println("some_key:", n)

// Lock to to write:
counter.Lock()
counter.m["some_key"]++
counter.Unlock()

Error Handling

f, err := os.Open("filename.ext")
if err != nil {
    log.Fatal(err)
}

Because error messages are often chained together, when writing error messages:

Panic and Recover

https://blog.golang.org/defer-panic-and-recover

Panic is a built-in function that stops the ordinary flow of control and begins panicking. When the function F calls panic, execution of F stops, any deferred functionsain F are executed normally, and then F returns to its caller. To the caller, F then behaves like a call to panic. The process continues up the stack until all functions in the current goroutine have returned, at which point the program crashes. Panics can be initiated by invoking panic directly. They can also be caused by runtime errors, such as out-of-bounds array accesses.

Recover is a built-in function that regains control of a panicking goroutine. Recover is only useful inside deferred functions. During normal execution, a call to recover will return nil and have no other effect. If the current goroutine is panicking, a call to recover will capture the value given to panic and resume normal execution.

Error Writing Strategies

Section 5.4 of The Go Programming Language describes five strategies of error writing.

  1. propagate the error from a subroutine up to the calling routine.
  2. retry some number of times, possibly waiting between each attempt. This makes sense for transient problems or delays that may resolve themselves.
  3. Log the error and gracefully end the program. Try to do this only from the main package.
  4. Log the error and continue, possibly with reduced functionality.
  5. Choose to safely ignore some errors. Very simple functions, where only one thing might go wrong, can return a boolean value rather than an error.
resp, err := http.Get(url)
if err != nil {
	return nil, err
}

// Add context to an error before propagating it upward:
doc, err := html.Parse(resp.Body)
resp.Body.Close()
if err != nil {
	// `fmt.Errorf` acts like `fmt.Sprintf`, but returns an `error` type value.
	// The `%w` verb in `Errorf` allows use of `errors.Unwrap`, `errors.Is`, and `errors.As`.
	return nil, fmt.Errorf("parsing %s as HTML: %w", url, err)
}

// Die (strategy 3):
log.Fatalf("Bad juju: %v", err)

// Return a boolean rather than an error (strategy 5):
value, ok := cache.Lookup(key)
if !ok {
	// … cache[key] does not exist.
}

Errors package

The errors package provides new functions for writing a library that exposes customized error types (or consuming a library that exposes helpfully typed errors):

var errEmptyRow = errors.New("empty row") // <------------
var errHeaderRow = errors.New("header row")

func readRow(s string) (payment, error) {
	var p payment

	if reEmptyLine.MatchString(s) {
		return p, errEmptyRow // <------------
	}
	if reLockboxHeader.MatchString(s) {
		return p, errHeaderRow
	}
	…
	return payment, err
}

func readFiles(files []string) ([]payment, error) {
	payments := make([]payment, 0, 100)

	for _, f := range files {
		file, err := os.Open(f)
		if err != nil {
			log.Fatal(err)
		}
		defer file.Close()

		line := 0
		scanner := bufio.NewScanner(file)
		for scanner.Scan() {
			line++
			p, err := parseLockboxRow(scanner.Text())
			if err != nil {
				if errors.Is(err, errHeaderRow) || errors.Is(err, errEmptyRow) { // <------------
					continue
				}
				return nil, fmt.Errorf("readFiles found bad line %d in file %s: %v", line, f, err)
			}
			payments = append(payments, p)
		}
		…
	}
	return payments, nil
}

Packages

package main

import (
	"fmt"
	"math"
	"os"

	// Alias package name, so we can use it here like `m.Pi`:
	m "example.com/math"

	// Import only for side-effects (i.e., triggering its `init` function on import):
	_ "github.com/mattn/go-sqlite3"
)

go get pulls remote dependencies, and stores them in the Go workspace:

$ go get example.com/paulgorman/example

It can update one package or all previously downloaded packages:

$ go get -u example.com/paulgorman/example
$ go get -u

Modules

See also https://blog.golang.org/go1.13-errors.

$ mkdir mymodule
$ cd mymodule
$ go mod init example.com/me/mymodule
go: creating new go.mod: module example.com/me/mymodule

$ go help modules | less

The module name need not correspond to a real, existing path as long as it’s unique.

Interfaces

Interfaces do a lot of work in Go. An interfaces specifies the type of an object by its behavior — if something can do this, it can be used here.

Interfaces involve two things: “interface types” and “interface values”.

An interface type lists the methods a concrete type needs in order to be considered an instance of that interface type.

type car interface {
	accelerate(i int) error
	brake()           error
}

An interface type establishes a contract based on its methods, but does not specify an implementation for the methods. A concrete type that wants to fulfill the interface contract is responsible for implementing the methods. Anything that implements the methods of an interface type can be treated as that type. For example, a dunebuggy type that implements both accelerate and brake methods can be passed to an oilchange function that accepts car types. Go’s Fprintf, for example, outputs to anything with a Write method.

type geometry interface {
	area() float64
	perim() float64
}

The interface lets us write a function that works on different types, so long as they all implement the interface’s methods:

type rect struct {
	width, height float64
}
type circle struct {
	radius float64
}

func (r rect) area() float64 {
	return r.width * r.height
}
func (r rect) perim() float64 {
	return 2*r.width + 2*r.height
}
func (c circle) area() float64 {
	return math.Pi * c.radius * c.radius
}
func (c circle) perim() float64 {
	return 2 * math.Pi * c.radius
}
func measure(g geometry) {
	fmt.Println(g)
	fmt.Println(g.area())
	fmt.Println(g.perim())
}

func main() {
	r := rect{width: 3, height: 4}
	c := circle{radius: 5}
	measure(r)
	measure(c)
}

In a loose analogy, if concrete types like int and string are nouns, and functions are verbs, then interfaces are adjectives. rect and circle are “geometry-ish”.

var x geometry
r2 := circle{radius: 9}
x = r2
x = rect{width:2, height:5}

Interfaces with only one or two methods are common in Go. The method often names the interface, like io.Writer for something that implements Write. A type can implement multiple interfaces.

Because interfaces decouple the definition from the implementation, definitions can appear in one package and implementations in a different package.

Interfaces can be used as fields in structures, passed as arguments to functions, and so forth.

The Empty Interface

The interface type that specifies zero methods is known as the empty interface (interface{}). An empty interface may hold values of any type. Empty interfaces are used by code that handles values of unknown type, like fmt.Printf.

func describe(i interface{}) {
	fmt.Printf("(%v, %T)\n", i, i)
}

func main() {
	var i interface{}
	describe(i) // (<nil>, <nil>)
	i = 42
	describe(i) // (42, int)
	i = "hello"
	describe(i) // (hello, string)
}

Type Assertions

t := x.(T)

This statement asserts that x holds a value of type T and assigns the underlying T value to the variable t. If x does not hold a T type value, the statement triggers a panic. To test whether an interface value holds a specific type, a type assertion can return two values: the underlying value and a boolean value that reports whether the assertion succeeded.

t, ok := x.(T)

If x holds a T, then t will be the underlying value and ok will be true. If not, ok will be false and t will be the zero value of type T, and no panic occurs. (Using t after a failed assertion panics, so check the value of ok first!)

Type switches

func do(i interface{}) {
	switch v := i.(type) {
	case int:
		fmt.Printf("Twice %v is %v\n", v, v*2)
	case string:
		fmt.Printf("%q is %v bytes long\n", v, len(v))
	default:
		fmt.Printf("I don't know about type %T!\n", v)
	}
}

func main() {
	do(21)
	do("hello")
	do(true)
}

The declaration in a type switch has the same syntax as a type assertion i.(T), but the keyword type replaces the specific type T.

Interface Example: Stringer

package main

import "fmt"

type IPAddr [4]byte

func (a IPAddr) String() string {
	return fmt.Sprintf("%d.%d.%d.%d", a[0], a[1], a[2], a[3])
}

func main() {
	hosts := map[string]IPAddr{
		"loopback":  {127, 0, 0, 1},
		"googleDNS": {8, 8, 8, 8},
	}
	for name, ip := range hosts {
		fmt.Printf("%v: %v\n", name, ip)
	}
}

Interface Example: Error

package main

import (
	"fmt"
	"time"
)

type MyError struct {
	When time.Time
	What string
}

func (e *MyError) Error() string {
	return fmt.Sprintf("at %v, %s", e.When, e.What)
}

func run() error {
	return &MyError{
		time.Now(),
		"it didn't work",
	}
}

func main() {
	if err := run(); err != nil {
		fmt.Println(err)
	}
}

Interface Example: Readers

https://golang.org/pkg/io/#Reader

The io package specifies the io.Reader interface, which represents the read end of a stream of data:

func (T) Read(b []byte) (n int, err error)

Read populates the given byte slice with data, and returns the number of bytes populated along with an error value. It returns an io.EOF error when the stream ends.

In one common pattern, an io.Reader wraps another io.Reader, modifying the stream in some way. For example, the gzip.NewReader function takes an io.Reader (a stream of compressed data) and returns a *gzip.Reader that also implements io.Reader (a stream of the decompressed data).

package main

import (
	"io"
	"os"
	"strings"
)

type rot13Reader struct {
	rdr io.Reader
}

func (rot *rot13Reader) Read(p []byte) (n int, err error) {
	n, err = rot.rdr.Read(p) // We call the "wrapped" io.Reader's Read() to fill p.
	for i, letter := range p[:n] {
		switch {
		case letter >= 'A' && letter <= 'Z':
			p[i] = 'A' + (letter - 'A' + 13) % 26
		case letter >= 'a' && letter <= 'z':
			p[i] = 'a' + (letter - 'a' + 13) % 26
		}
	}
	return n, err
}

func main() {
	s := strings.NewReader("Lbh penpxrq gur pbqr!")
	r := rot13Reader{s}
	io.Copy(os.Stdout, &r)
}

Concurrency

See also https://paulgorman.org/technical/golang-concurency.txt.html

Go provides concurrency using goroutines and channels.

An OS process has a PID and a block of memory. An OS thread is less expensive than forking an additional process; threads share the memory, open files, and PID of their parent.

A goroutine is like a thread scheduled by Go instead of by the OS. Goroutines have much lower overhead than OS threads. Go generally doesn’t start a new OS thread for a goroutine. The Go runtime holds a number of threads and multiplexes scheduling goroutines between them; if a goroutine blocks, Go swaps it out to let another goroutine execute on that thread. Goroutines are a lightweight abstraction on top of threads; a Go program doesn’t deal with treads, and the OS isn’t aware of goroutines.

Rob Pike’s talk about concurrency is instructive: https://www.youtube.com/watch?v=cN_DpYBzKso

Start a goroutine using the go keyword.

Because goroutines run in the same address space, control access to shared memory. Concurrent reading may be safe, but certainly synchronize whenever concurrently writing values. For simple cases, a mutex may be enough (i.e., sync.Mutex).

Channels provide another way to safely sharing memory. Go’s preference for channels is often stated as:

Don’t communicate by sharing memory; share memory by communicating.

A channel is a bit like a pipe in a shell, but it has a type. The type matches the kind of data to be passed through the channel.

ch := make(chan int) // A channel for int data.
func worker(ch chan int) { … } // Pass the channel to a function as type "chan int".

Channels support sending and receiving, with direction indicated using arrow notation:

ch <- d // Send data to channel.
d := <-ch // Receive data from channel.

Note that sending data to and receiving data from a channel is blocking; execution of the goroutine pauses until data flows across the channel when the other side is ready. This allows goroutines to synchronize without explicit locks.

package main

import (
	"fmt"
	"time"
	"math/rand"
)

type Worker struct {
	id int
}

func (w *Worker) process(c chan int) {
	for {
		data := <-c
		fmt.Printf("worker %d got %d\n", w.id, data)
	}
}

func main() {
	c := make(chan int)
	for i := 0; i < 5; i++ {
		worker := &Worker{id: i}
		go worker.process(c)
	}
	for {
		c <- rand.Int()
		time.Sleep(time.Millisecond * 50)
	}
}

What if we have multiple channels, and want to read from the one with pending data? Go’s select statement is like switch, but the decision is based on the ability to communicate rather than value tests. select lets a goroutine wait on multiple communication channels. If no channel is ready to communicate, select falls through to the default clause.

func fibonacci(c, quit chan int) {
	x, y := 0, 1
	for {
		select {
		case c <- x:
			x, y = y, x+y
		case <-quit:
			fmt.Println("quit")
			return
		}
	}
}

func main() {
	c := make(chan int)
	quit := make(chan int)
	go func() {
		for i := 0; i < 10; i++ {
			fmt.Println(<-c)
		}
		quit <- 0
	}()
	fibonacci(c, quit)
}

We can make a channel buffer. Sends to a buffered channel only blocks when the buffer fills; receives only block when its empty.

func main() {
	ch := make(chan int, 2)
	ch <- 1
	ch <- 2
	ch <- 3 // Sending too much to a buffered channel causes a fatal deadlock.
	fmt.Println(<-ch)
	fmt.Println(<-ch)
}

Sometimes channels send events (i.e., the occurrence of the signal is the only thing that’s important, not the data sent). Although we can send a tiny boolean for the signal, a Go idiom is to use a struct. The empty struct emphasizes the “event” nature of the channel.

func main() {
	// [Set up some stuff.]
	done := make(chan struct{})
	go func() {
		// [Do some stuff.]
		log.Println("done")
		done <- struct{}{} // Signal the main goroutine with an empty struct.
	}()
	// [Do other stuff.]
	<-done // Wait the background goroutine to end.
}

Range and Close

A sender can close a channel to indicate that no more values will be sent. Receivers test whether a channel has been closed by assigning a second parameter to the receive expression:

v, ok := <-ch

ok is false if there are no more values to receive and the channel is closed.

The loop for i := range c receives values from the channel repeatedly until it is closed.

Only the sender should close a channel, never the receiver, because sending on a closed channel causes a panic.

Channels aren’t like files; they don’t usually need to close them. Closing is only necessary when the receiver must be told there are no more values coming (e.g., to end a range loop).

package main

import "fmt"

func fibonacci(n int, c chan int) {
	x, y := 0, 1
	for i := 0; i < n; i++ {
		c <- x
		x, y = y, x+y
	}
	close(c)
}

func main() {
	c := make(chan int, 10)
	go fibonacci(cap(c), c)
	for i := range c {
		fmt.Println(i)
	}
}

sync.Mutex

Channels are great for communication among goroutines, but sometimes they need to share memory. Mutual exclusion is a simple method to avoid conflicts when multiple goroutines access a variable.

package main

import (
	"fmt"
	"sync"
	"time"
)

// SafeCounter is safe to use concurrently.
type SafeCounter struct {
	v   map[string]int
	mux sync.Mutex
}

// Inc increments the counter for the given key.
func (c *SafeCounter) Inc(key string) {
	c.mux.Lock()
	// Lock so only one goroutine at a time can access the map c.v.
	c.v[key]++
	c.mux.Unlock()
}

// Value returns the current value of the counter for the given key.
func (c *SafeCounter) Value(key string) int {
	c.mux.Lock()
	// Lock so only one goroutine at a time can access the map c.v.
	defer c.mux.Unlock()
	return c.v[key]
}

func main() {
	c := SafeCounter{v: make(map[string]int)}
	for i := 0; i < 1000; i++ {
		go c.Inc("somekey")
	}

	time.Sleep(time.Second)
	fmt.Println(c.Value("somekey"))
}

The << and >> Operators (left shift and right shift)

https://golang.org/ref/spec#Arithmetic_operators

The shift operators shift the left operand by the shift count specified by the right operand. They implement arithmetic shifts if the left operand is a signed integer and logical shifts if it is an unsigned integer. There is no upper limit on the shift count. Shifts behave as if the left operand is shifted n times by 1 for a shift count of n. As a result, x << 1 is the same as x*2 and x >> 1 is the same as x/2 but truncated towards negative infinity.

https://stackoverflow.com/questions/5801008/go-and-operators

The super (possibly over) simplified definition is just that << is used for “times 2” and >> is for “divided by 2” - and the number after it is how many times. So n << x is “n times 2, x times”. And y >> z is “y divided by 2, z times”. For example, 1 << 5 is “1 times 2, 5 times” or 32. And 32 >> 5 is “32 divided by 2, 5 times” or 1.

Testing

See also https://paulgorman.org/technical/golang-testing.txt.html

Go includes a lightweight testing framework comprised of the testing package and the go test tool. Create a test file with a name ending in _test.go. The _test.go file should contain functions named TestFOO with the signature:

func (t *testing.T)

If the test function calls a failure function like t.Error or t.Fail, Go considers the test failed.

For a package named stringutil with a Reverse function, create a stringutil_test.go file:

package stringutil

import "testing"

func TestReverse(t *testing.T) {
	cases := []struct {
		in, want string
	}{
		{"Hello, world", "dlrow ,olleH"},
		{"Hello, 世界", "界世 ,olleH"},
		{"", ""},
	}
	for _, c := range cases {
		got := Reverse(c.in)
		if got != c.want {
			t.Errorf("Reverse(%q) == %q, want %q", c.in, got, c.want)
		}
	}
}

… and run the test:

$ go test example.com/stringutil

Init

Each source file may define one or more init functions (run in the order they appear in the code). Go calls init after initialization of global variable declarations and initialization of imported packages. Importing a package runs the package’s init functions.

func init() {
	if user == "" {
		log.Fatal("$USER not set")
	}
}

Time

The time.Format function uses a reference time: Mon Jan 2 15:04:05 -0700 MST 2006 (i.e., 01/02 03:04:05PM ‘06 -0700, or 0 1 2 3 4 5 6 7).

Git pre-commit hooks

$ touch ~/repo/go/src/example.com/myproject/.git/hooks/pre-commit
$ chmod a+x ~/repo/go/src/example.com/myproject/.git/hooks/pre-commit
$ cat << 'EOF' > ~/repo/go/src/example.com/myproject/.git/hooks/pre-commit
#!/bin/sh
go fmt ./...
go vet ./...
go test -cover ./...
find -type f -name '*.go' -exec wc -l {} +
find -type f -name '*.go' -exec sed '/^\s*$/d' {} + | wc -l; echo ' ↳ total (non-blank) lines of code'
EOF

Building and Build Flags

When building binaries for distribution, we may want to strip debug symbols to reduce the binary size:

% go build -ldflags '-s -w'

To build a static binary, which can be useful when moving it to a musl system:

5 CGO_ENABLED=0 go build -a -installsuffix cgo

While go build compiles the executable and writes it to the same directory, go install does a little more. go install moves the compiled binary to $GOPATH/bin/, and does some caching of imported packages.

Cross-Compiling

$ GOOS=windows GOARCH=amd64 go build
$ GOOS=openbsd GOARCH=386 go build
$ GOOS=linux GOARCH=386 go build
$ GOOS=freebsd GOARCH=arm64 go build

Other Go Tooling

$ go env
GOARCH="amd64"
GOBIN=""
GOCACHE="/home/paul/.cache/go-build"
GOEXE=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOOS="linux"
[…]

$ go list -m all
example.org/inventory
github.com/bvinc/go-sqlite-lite v0.5.0

$ go mod why -m github.com/bvinc/go-sqlite-lite
# github.com/bvinc/go-sqlite-lite
example.org/inventory
github.com/bvinc/go-sqlite-lite/sqlite3

$ go mod tidy
$ go mod verify

$ go clean -modcache

$ go doc -src strings.Replace

$ go test .          # Run all tests in the current directory
$ go test ./...      # Run all tests in the current directory and sub-directories
$ go test ./foo/bar  # Run all tests in the ./foo/bar directory
$ go test -race ./...
$ go test -cover ./...

$ go vet ./...

$ go fix ./...