Go Tour

Notes from a re-read in July 2021

  • [5]int is an array, []int is a slice. Create slices with make(). A slice has len() elements; it’s capacity is the length of the underlying array starting from the first element of the slice.

  • Maps are built in, use make to create them: make(map[string]string). Map access returns the zero value for the value type if the key isn’t present.

  • Anonymous functions and closures are supported

  • Value receivers pass-by-value (copy), pointer receivers pass-by-reference.

  • Interfaces define a set of methods. No explicit implementation - any type with those methods is automatically an implementer.

  • Note that an interface value that holds a nil concrete value is itself non-nil. An interface value holds info about an underlying type & and underlying value. It is nil only when both of these are nil. If it has an underlying type and a nil underlying value (👇) then it isn’t nil. Docs: https://golang.org/doc/faq#nil_error

    // I is an interface type, T is a struct that implements I
    var t *T
    var i I = t
    fmt.Println(t, i, t == nil, i == nil)
    // prints: <nil> <nil> true false 😬
    // so i isn't nil even though it appears to be when printed (because 
    // it resolves to the underlying instance, probably)
    
  • An empty interface may hold values of any type.

  • t, ok := i.(T) is a type assertion that checks if i is a T. Works with a switch using the type keyword.

  • The Stringer interface is like fmt::Display, and there’s an error interface

  • A common pattern is an io.Reader that wraps another io.Reader, modifying the stream in some way.

  • Create channels with make: make(chan int, 100). range ch receives values until the channel is closed.

  • Use select with default: to implement a timeout.

  • You can’t pass a []net.Conn to a function that expects []io.Writer even though net.Conn implements io.Writer. The only way is to create a new slice that explicitly typed as the other type

  • Use a Timer to run something in the future, a Ticker to run something repeatedly.

  • Use a WaitGroup to wait for a bunch of goroutines to finish. main will happily exit even if there are goroutines running, so this is very useful.

  • The atomic package allows for atomic operations on primitive types.

  • The sync package has a bunch of concurrency utils, including a mutex and a concurrent map.

  • The list package has a linked list impl.

  • String formatting guide

  • Use Context to carry cancellation/deadline/timeout signals across goroutines. A context’s Done() channel will trigger at the deadline / after the timeout expires, but it’s on the client goroutine to actually read from it and cancel itself.

  • The syscall package allows low-level kernel access. Not sure if this allows direct syscall access or if there’s a libc/etc. in between.

Original Notes

Structs

A struct literal denotes a newly allocated struct value by listing the values of its fields. You can list just a subset of fields by using the Name: syntax. (And the order of named fields is irrelevant.) The special prefix & returns a pointer to the struct value.

type Vertex struct {
	X, Y int
}

var (
	v1 = Vertex{1, 2}  // has type Vertex
	v2 = Vertex{X: 1}  // Y:0 is implicit
	v3 = Vertex{}      // X:0 and Y:0
	p  = &Vertex{1, 2} // has type *Vertex
)

Embedding Structs

The bufio package has two struct types, bufio.Reader and bufio.Writer, each of which of course implements the analogous interfaces from package io. And bufio also implements a buffered reader/writer, which it does by combining a reader and a writer into one struct using embedding: it lists the types within the struct but does not give them field names.

// ReadWriter stores pointers to a Reader and a Writer.
// It implements io.ReadWriter.
type ReadWriter struct {
    *Reader  // *bufio.Reader
    *Writer  // *bufio.Writer
}

The embedded elements are pointers to structs and of course must be initialized to point to valid structs before they can be used. The ReadWriter struct could be written as

type ReadWriter struct {
    reader *Reader
    writer *Writer
}

but then to promote the methods of the fields and to satisfy the io interfaces, we would also need to provide forwarding methods, like this:

func (rw *ReadWriter) Read(p []byte) (n int, err error) {
    return rw.reader.Read(p)
}

By embedding the structs directly, we avoid this bookkeeping. The methods of embedded types come along for free, which means that bufio.ReadWriter not only has the methods of bufio.Reader and bufio.Writer, it also satisfies all three interfaces: io.Reader, io.Writer, and io.ReadWriter.

Methods / Functions

Remember: a method is just a function with a receiver argument.

func (v Vertex) Abs() float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func (v *Vertex) Scale(f float64) {
	v.X = v.X * f
	v.Y = v.Y * f
}

func Scale(v *Vertex, f float64) {
	v.X = v.X * f
	v.Y = v.Y * f
}

func main() {
	v := Vertex{3, 4}
	v.Scale(10)
	Scale(&v, 10)
	fmt.Println(v.Abs())
}

Named Return Parameters

Because named results are initialized and tied to an unadorned return, they can simplify as well as clarify. Here’s a version of io.ReadFull that uses them well:

func ReadFull(r Reader, buf []byte) (n int, err error) {
    for len(buf) > 0 && err == nil {
        var nr int
        nr, err = r.Read(buf)
        n += nr
        buf = buf[nr:]
    }
    return
}

Pointer receivers

You can declare methods with pointer receivers. This means the receiver type has the literal syntax *T for some type T. (Also, T cannot itself be a pointer such as *int.)

Methods with pointer receivers can modify the value to which the receiver points (as Scale does here). Since methods often need to modify their receiver, pointer receivers are more common than value receivers.

For the statement v.Scale(5), even though v is a value and not a pointer, the method with the pointer receiver is called automatically. That is, as a convenience, Go interprets the statement v.Scale(5) as (&v).Scale(5) since the Scale method has a pointer receiver.

Defer

The arguments to the deferred function (which include the receiver if the function is a method) are evaluated when the defer executes, not when the call executes.

Interfaces

The Empty Interface

The interface type that specifies zero methods is known as the empty interface:

interface{}

Embedding Interfaces

type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}


// ReadWriter is the interface that combines the Reader and Writer interfaces.
type ReadWriter interface {
    Reader
    Writer
}

An empty interface may hold values of any type. (Every type implements at least zero methods.) Empty interfaces are used by code that handles values of unknown type. For example, fmt.Print takes any number of arguments of type interface{}.

Concurrency

Do not communicate by sharing memory; instead, share memory by communicating.

Although Go’s approach to concurrency originates in Hoare’s Communicating Sequential Processes (CSP), it can also be seen as a type-safe generalization of Unix pipes.

Goroutines

A goroutine is a lightweight thread managed by the Go runtime.

go f(x, y, z) starts a new goroutine running f(x, y, z)

Goroutines run in the same address space, so access to shared memory must be synchronized. The sync package provides useful primitives, although you won’t need them much in Go as there are other primitives.

Goroutines are multiplexed onto multiple OS threads so if one should block, such as while waiting for I/O, others continue to run. Their design hides many of the complexities of thread creation and management.

Channels

Channels are a typed conduit through which you can send and receive values with the channel operator, <-. (The data flows in the direction of the arrow.)

ch <- v    // Send v to channel ch.
v := <-ch  // Receive from ch, and assign value to v.

By default, sends and receives block until the other side is ready. This allows goroutines to synchronize without explicit locks or condition variables.

Channels can be buffered. Provide the buffer length as the second argument to make to initialize a buffered channel.

Select

The select statement lets a goroutine wait on multiple communication operations. A select blocks until one of its cases can run, then it executes that case. It chooses one at random if multiple are ready.

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
		}
	}
}

Collections

Arrays

The type [n]T is an array of n values of type T.

var a [10]int

An array’s length is part of its type, so arrays cannot be resized. This seems limiting, but don’t worry; Go provides a convenient way of working with arrays.

If you pass an array to a function, it will receive a copy of the array, not a pointer to it.

Slices

A slice, on the other hand, is a dynamically-sized, flexible view into the elements of an array. In practice, slices are much more common than arrays. The type []T is a slice with elements of type T.

//  A slice is formed by specifying a low and high bound
a[low : high]

// Create a slice which includes elements 1 through 3
a[1:4]

A slice does not store any data, it just describes a section of an underlying array.

The length of a slice may be changed as long as it still fits within the limits of the underlying array; just assign it to a slice of itself.

Append

The signature of append is different from our custom Append function above. Schematically, it’s like this:

func append(slice []T, elements ...T) []T

where T is a placeholder for any given type. You can’t actually write a function in Go where the type T is determined by the caller. That’s why append is built in: it needs support from the compiler.

☹️

Literals

A slice literal is like an array literal without the length.

// This is an array literal:
[3]bool{true, true, false}

// And this creates the same array as above, then builds a slice that references it:
[]bool{true, true, false}

// Slice literals can contain multiple types
[]interface{}{1,"something",false}

Strings

For strings, the range does more work for you, breaking out individual Unicode code points by parsing the UTF-8. Erroneous encodings consume one byte and produce the replacement rune U+FFFD.

for pos, char := range "日本\x80語" { // \x80 is an illegal UTF-8 encoding
    fmt.Printf("character %#U starts at byte position %d\n", char, pos)
}

// prints:
// character U+65E5 '日' starts at byte position 0
// character U+672C '本' starts at byte position 3
// character U+FFFD '�' starts at byte position 6
// character U+8A9E '語' starts at byte position 7

Modules

Go programs are organized into packages. A package is a collection of source files in the same directory that are compiled together. Functions, types, variables, and constants defined in one source file are visible to all other source files within the same package.

A module is a collection of related Go packages that are released together. A Go repository typically contains only one module, located at the root of the repository. A file named go.mod there declares the module path: the import path prefix for all packages within the module.

Does this imply that a typical Go project uses a single package without any real namespacing? 🤔 Based on a few real Go repos — caddy & nomad — the answer appears to be no, a repository typically does contain multiple modules.

Memory

Go has two allocation primitives, the built-in functions new and make.

(new), unlike its namesakes in some other languages it does not initialize the memory, it only zeros it. That is, new(T) allocates zeroed storage for a new item of type T and returns its address, a value of type *T. In Go terminology, it returns a pointer to a newly allocated zero value of type T.

The built-in function make(T, args) serves a purpose different from new(T). It creates slices, maps, and channels only, and it returns an initialized (not zeroed) value of type T (not *T). The reason for the distinction is that these three types represent, under the covers, references to data structures that must be initialized before use.

var p *[]int = new([]int)       // allocates slice structure; *p == nil; rarely useful
var v  []int = make([]int, 100) // the slice v now refers to a new array of 100 ints

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

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

Misc.

The init function

Finally, each source file can define its own niladic init function to set up whatever state is required. (Actually each file can have multiple init functions.) And finally means finally: init is called after all the variable declarations in the package have evaluated their initializers, and those are evaluated only after all the imported packages have been initialized.

Recover

When panic is called, including implicitly for run-time errors such as indexing a slice out of bounds or failing a type assertion, it immediately stops execution of the current function and begins unwinding the stack of the goroutine, running any deferred functions along the way. If that unwinding reaches the top of the goroutine’s stack, the program dies. However, it is possible to use the built-in function recover to regain control of the goroutine and resume normal execution.

Edit