Go Tour
Notes from a re-read in July 2021
-
[5]int
is an array,[]int
is a slice. Create slices withmake()
. A slice haslen()
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 arenil
. If it has an underlying type and anil
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 ifi
is aT
. Works with aswitch
using thetype
keyword. -
The
Stringer
interface is likefmt::Display
, and there’s anerror
interface -
A common pattern is an
io.Reader that
wraps anotherio.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
withdefault:
to implement a timeout. -
You can’t pass a
[]net.Conn
to a function that expects[]io.Writer
even thoughnet.Conn
implementsio.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, aTicker
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. -
Use
Context
to carry cancellation/deadline/timeout signals across goroutines. A context’sDone()
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 statementv.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 runningf(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.