Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Preface

This book teaches Go to someone who already programs. If you have written loops and functions in Lux, Python, JavaScript, or anything of that family, you have what you need. You will not be told what a variable is. You will be told what Go does with one, a shorter conversation than in most languages and a more useful one than you expect.

Go is small, and it stays small deliberately. There is one loop keyword, one way to format source, no classes, no exceptions, and no inheritance. A language that size can be held in your head entire, and that is the thing Go is buying: you spend your attention on the problem rather than on the language, and a program someone else wrote reads like one you wrote. The cost is that Go declines to do some work for you. A function that can fail hands the failure back as an ordinary value and you deal with it on the next line. A value gets copied when you pass it, unless you ask for a pointer. Nothing checks that you covered every case. Coming from a scripting language you will notice the typing first, but it is this second habit — the language handing jobs back rather than absorbing them — that takes the longest to settle into, and Chapters 5 and 8 are where it gets its due.

What you get in return arrives early. The compiler is fast enough that building is not a pause in your work. gofmt ends every argument about layout by reformatting your source to the one true shape. The standard library is broad enough that most programs need no dependencies at all, and the tool that builds, tests, formats, and documents your code is the same go command with a different first word. Then there is concurrency, Go’s headline and Chapter 11: a goroutine costs a keyword and a few kilobytes, so structuring a program around dozens of concurrent pieces is an ordinary decision rather than an architectural one.

The pace is brisk on purpose. Programs come first and explanation follows, and the explanation stops when the point has been made. A feature you can already guess the shape of gets a sentence. A feature that will bite you gets a page. When you want the complete description of some corner of the language, the Go Programming Language Specification is short, readable, and free, and this book is not trying to replace it.

Read it with a keyboard. Type the programs in, run them, then break them and read what the compiler says. The exercises are short and most are variations on the program just presented; do the ones that look interesting and skip the rest without guilt.

Getting the compiler

Install Go from https://go.dev/dl. That gives you the go command: the compiler, the test runner, the formatter, the package manager, and the documentation browser. Check it with go version.

The code here needs Go 1.23 or newer, released in August 2024. The newest things it uses are ranging over an integer and the iterator forms in the slices and maps packages. Where a program’s output depends on your machine, the text says so.

About the examples

Every complete program in this book lives in the examples directory beside the text, one directory per program. From examples, running

go run ./ch01/fahr2

builds and runs the Fahrenheit table from Chapter 1. The programs are the ones printed here, not adaptations, so if something does not behave the way the text claims, the text is wrong and you should trust the compiler.

The programs that deliberately fail to compile live in examples/errors, one directory per mistake. Every compiler message quoted in this book came from running go build on the file of that name, and Appendix C collects them.

A note if you are coming from Lux

Lux and Go are close in the places that matter for getting started and far apart in two specific spots. Close: types are written after the name, functions are func, a for loop over a range reads almost the same, and structs are structs.

The two that are far apart are the ones Lux was careful about. Lux has no null; Go has nil, and reaching for it is normal rather than a smell. Lux has enums with associated values and a match that must cover every case; Go has neither, and Chapter 9 shows what you build instead. Neither of those is Go being careless. They are a different bet about where safety comes from — the compiler in one language, the programmer and the tooling in the other — and the way to learn Go is to make the bet on purpose rather than to keep reaching for the guarantee that is not there. lux convert go program.lux prints the Go translation of any Lux program you have written, and reading a few of those against the originals is the fastest map between the two. Appendix B has the construct-by-construct table.

1. A Tutorial Introduction

This chapter is a tour. It moves fast, shows real programs, and leaves the careful definitions for later chapters. The goal is to get enough Go into your hands that the rest of the book has something to build on. Anything you do not fully understand here is explained properly further along, and the text says where.

1.1 Getting Started

The only way to start.

package main

import "fmt"

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

Put that in a file called hello.go and run go run hello.go. It compiles the program to a temporary file, runs it, and throws the binary away. go build hello.go keeps the binary instead, producing an executable called hello that depends on nothing and can be copied to another machine with the same operating system and run there.

In practice you work in a module rather than on loose files:

go mod init hello

That writes a go.mod naming the module and the Go version it expects. Once one exists, go run . builds the package in the current directory, go build ./... builds everything beneath it, and go get can add dependencies. Every program in this book lives in the module under examples.

The program is three ideas. package main says this file belongs to the package named main, the one that produces an executable; any other name produces a library. import "fmt" pulls in the formatted-I/O package from the standard library. func main() is the function the system calls to start the program, and a main package must have one.

Before you run anything else, run gofmt -w hello.go. It rewrites the file in Go’s standard layout: tabs for indentation, braces where Go puts braces, imports sorted. Nobody configures it, because it takes no options. Every Go project you will ever read has been through it, so they all look alike, and arguments about formatting are simply not a thing that happens. Most editors run it on save; set that up now and never think about it again.

Exercise 1-1. Run the program. Then delete the import line and compile again. Read the error.

Exercise 1-2. Print several lines. Find out what \n, \t, and \" do inside the quotes, and what backquotes do instead of quotes.

1.2 Variables and Arithmetic Expressions

The next program prints a table of Fahrenheit temperatures and their Celsius equivalents, using the formula C = (5/9)(F−32).

// Print a Fahrenheit-Celsius table for fahr = 0, 20, ..., 300.
package main

import "fmt"

func main() {
	const lower, upper, step = 0, 300, 20

	fahr := lower
	for fahr <= upper {
		celsius := 5 * (fahr - 32) / 9
		fmt.Printf("%d\t%d\n", fahr, celsius)
		fahr += step
	}
}

It prints:

0	-17
20	-6
40	4
...
300	148

Comments run from // to the end of the line, and /* ... */ spans lines. A comment directly above a top-level declaration is its documentation, which go doc will show to anyone who asks about it, so write those two facing the reader rather than yourself.

const introduces values fixed at compile time; lower, upper, and step never change, so they are constants rather than variables. The line declares three of them at once.

fahr := 0 is the short declaration. It declares a variable and infers its type from the value, and it is how Go programmers declare nearly everything. The long form is var fahr int = 0, useful when you want to name the type, and var fahr int alone gives you Go’s zero value for that type: 0 for numbers, "" for strings, and false for booleans. Every variable in Go starts out as something; there is no uninitialized memory and no undefined.

Two things about := will catch you. It only works inside a function, so package-level declarations use var or const. And it must declare at least one new name, so a second := on the same variable is an error rather than an assignment:

shortdecl/main.go:7:7: no new variables on left side of :=

Now the surprise that catches everyone. Declare a variable and fail to use it, and the program does not compile:

unusedvar/main.go:7:2: declared and not used: total

An unused import is the same:

unusedimport/main.go:5:2: "os" imported and not used

These are errors, not warnings, and there is no flag to soften them. It feels petty for about a week and then it stops registering, because the thing it prevents is real: unused names are the residue of a change you did not finish, and in a language that lets them slide they accumulate until nobody can tell which are dead. When you genuinely want a value you are not using yet, assign it to _, the blank identifier, which discards it.

The arithmetic is where this program lies to you. 5 * (fahr - 32) / 9 is integer arithmetic throughout, so the division truncates toward zero and the table is wrong in the last digit. C has the same trap and K&R spends the same paragraph on it. The fix is floating point.

1.3 Formatted Output

// The same table in floating point, with the columns lined up.
package main

import "fmt"

func main() {
	const lower, upper, step = 0.0, 300.0, 20.0

	fahr := lower
	for fahr <= upper {
		celsius := (fahr - 32.0) * 5.0 / 9.0
		fmt.Printf("%3.0f %6.1f\n", fahr, celsius)
		fahr += step
	}
}
  0  -17.8
 20   -6.7
 40    4.4
...
300  148.9

Printf formats its arguments according to a template, in the C tradition, and its verbs are worth learning early because you will use them all day. %d is an integer, %f a float, %s a string, %t a boolean. %v is any value in its default format and %+v adds field names when the value is a struct, which between them cover most debugging. %q is a quoted string, which shows you the whitespace you are staring past. %T prints the value’s type rather than the value, and is the fastest way to answer “what did that expression actually give me”. A literal percent sign is %%.

Between the % and the verb goes the width and precision: %6.1f means at least six characters wide with one digit after the point, and a minus sign as in %-6s left-justifies instead. That is what lines the columns up above.

Println needs no template. It prints its arguments separated by spaces with a newline at the end, and it is what you reach for when the layout does not matter. fmt.Sprintf returns the formatted string instead of printing it.

Notice that the constants changed from 0 to 0.0. Go will not mix numeric types in an expression, and it will not quietly convert one to the other:

mismatch/main.go:8:14: invalid operation: n * f (mismatched types int and float64)

Every conversion is written out, as float64(n). This is tedious in the small and worth it in the large, because it means the type of every expression is readable from the expression itself.

Exercise 1-3. Add a heading to the table.

Exercise 1-4. Print a Celsius-to-Fahrenheit table.

1.4 The For Statement

There are many ways to write any program. Here is the table again, counting down, in the form a Go programmer would actually use.

// The table again, as a Go programmer would write it.
package main

import "fmt"

func main() {
	for fahr := 300; fahr >= 0; fahr -= 20 {
		fmt.Printf("%3d %6.1f\n", fahr, (float64(fahr)-32)*5/9)
	}
}

Go has one loop keyword, and for covers every shape other languages spread across three. The full form has an initializer, a condition, and a post statement, with no parentheses around them and non-optional braces:

for i := 0; i < 10; i++ {

Drop the first and third and you have a while:

for fahr <= upper {

Drop all three and you have a loop that runs until something inside it breaks:

for {

Ranging over something gives you the elements. Over a slice or array you get index and value, over a map key and value, over a string index and rune, over a channel each value sent, and over an integer the numbers below it:

for i := range 10 {

That last one is how power counts in section 1.8. When you want only the second half of a two-value range, _ discards the first: for _, v := range xs.

i++ is a statement rather than an expression, so you cannot write x = i++, and there is no ++i. Assignment operators like += work as you would expect.

1.5 Character Input and Output

This program copies its input to its output, a line at a time.

// Copy standard input to standard output, a line at a time.
package main

import (
	"bufio"
	"fmt"
	"os"
)

func main() {
	in := bufio.NewScanner(os.Stdin)
	for in.Scan() {
		fmt.Println(in.Text())
	}
}

os.Stdin is the program’s input as an open file. bufio.NewScanner wraps it in a scanner that buffers reads and splits the stream into tokens, lines by default. Scan advances to the next token and reports whether it found one, so the loop ends at end of input; Text returns the token it found, without the newline. Splitting on words instead is one call: in.Split(bufio.ScanWords).

Two habits to pick up now. Reading a whole file into memory with os.ReadFile is fine for a configuration file and a bad idea for a log, while a scanner works the same on both. And Println writes to standard output while fmt.Fprintln(os.Stderr, ...) writes to standard error, where diagnostics belong so that a program’s real output stays pipeable.

1.6 Counting

The Unix wc program counts lines, words, and characters. Here it is.

// Count lines, words, and characters on standard input.
package main

import (
	"bufio"
	"fmt"
	"os"
	"strings"
)

func main() {
	lines, words, chars := 0, 0, 0

	in := bufio.NewScanner(os.Stdin)
	for in.Scan() {
		line := in.Text()
		lines++
		words += len(strings.Fields(line))
		chars += len(line) + 1
	}

	fmt.Printf("%d %d %d\n", lines, words, chars)
}
$ go run ./ch01/count < gettysburg.txt
4 54 309

Which agrees with the real wc. Three variables are declared and assigned on one line, which reads well when they belong together and badly when they do not. strings.Fields splits on runs of whitespace and returns a slice of the pieces, so len of it is the word count. The character count adds one per line for the newline the scanner removed.

chars counts bytes, not characters, and the two are the same only while the input is ASCII. Go strings are UTF-8, and Chapter 7 takes that seriously.

Exercise 1-5. Print the longest line in the input.

Exercise 1-6. Count how many times each word appears. You will want a map, covered in section 7.3; try it after reading that.

1.7 Functions

// Test the power function.
package main

import "fmt"

// power raises base to the n-th power, for n >= 0.
func power(base, n int) int {
	p := 1
	for range n {
		p *= base
	}
	return p
}

func main() {
	for i := range 10 {
		fmt.Printf("%d %6d %6d\n", i, power(2, i), power(-3, i))
	}
}
0      1      1
1      2     -3
2      4      9
...
9    512 -19683

A function declaration is func, the name, the parameters with their types, and the result type. Where consecutive parameters share a type you write it once, so (base, n int) is two integers. The result type goes after the parameter list, where Lux writes -> int and C writes the type in front.

Go functions can return more than one value, and a function that might fail reports it that way, a convention running through the whole standard library. Chapters 4 and 8 are about it.

Names starting with a capital letter are exported from their package and visible to code that imports it; names starting lower-case are private to the package. Println is capitalized for that reason and power is not. Case is the only access-control mechanism Go has, so you can see a name’s visibility at every use rather than only at its declaration.

1.8 Command-Line Arguments

// Echo the command-line arguments.
package main

import (
	"fmt"
	"os"
	"strings"
)

func main() {
	fmt.Println(strings.Join(os.Args[1:], " "))
}

os.Args is a slice of strings holding the command line, with the program’s own name at index 0, so the arguments proper are os.Args[1:]. Slicing is section 7.1. For anything beyond a couple of positional arguments the standard library’s flag package parses options for you, and Chapter 10 uses it.

Exercise 1-7. Write a program that prints its arguments one per line, with the index of each.

Exercise 1-8. Write echo: print the arguments separated by spaces, but accept a -n first argument meaning “no trailing newline”.

1.9 What You Have

You can now write a Go program that reads input, does arithmetic, loops, branches, calls functions you wrote, and prints results. That is most of what any program does. The rest of this book is about the parts Go asks you to be deliberate about: what a type is and what conversions cost, what happens when you pass a value to a function, how a program says that something went wrong, and how to write code that works on more than one type. Then the tooling, and then concurrency.

2. Types, Operators, and Expressions

Every value in Go has a type, fixed when the program is compiled and never changing afterwards. A scripting language decides what x + y means while the program runs; Go decides while the program is built, refuses the ones that make no sense, and generates code that does not have to check. This chapter is the type system’s ground floor.

2.1 The Basic Types

The numeric types come in explicit widths. Signed integers are int8, int16, int32, int64; unsigned are uint8 through uint64; floats are float32 and float64. There are complex types and you can ignore them.

Two of those have second names. byte is another spelling of uint8 and rune is another spelling of int32, used when the value is a raw byte or a Unicode code point respectively. These are true aliases and not distinct types, so a byte and a uint8 mix freely.

Then there are int and uint, which are the sizes the machine prefers: 64 bits on any machine you are likely to use, though the language permits 32. Use int unless you have a reason not to. It is what array indexes and lengths are, it is what an integer literal becomes, and reaching for int64 because the number might be large is a mistake on a 64-bit machine and a deliberate choice about file formats and wire protocols everywhere else.

// What the numeric types can hold.
package main

import (
	"fmt"
	"math"
	"unsafe"
)

func main() {
	var i int
	fmt.Printf("int      %d bytes  %d .. %d\n",
		unsafe.Sizeof(i), math.MinInt, math.MaxInt)
	fmt.Printf("int8     1 byte   %d .. %d\n", math.MinInt8, math.MaxInt8)
	fmt.Printf("uint8    1 byte   0 .. %d\n", math.MaxUint8)
	fmt.Printf("int32    4 bytes  %d .. %d\n", math.MinInt32, math.MaxInt32)
	fmt.Printf("float64  8 bytes  largest %g, smallest step above 1 is %g\n",
		math.MaxFloat64, math.Nextafter(1, 2)-1)
}
int      8 bytes  -9223372036854775808 .. 9223372036854775807
int8     1 byte   -128 .. 127
uint8    1 byte   0 .. 255
int32    4 bytes  -2147483648 .. 2147483647
float64  8 bytes  largest 1.7976931348623157e+308, smallest step above 1 is 2.220446049250313e-16

bool is true or false, and nothing converts to it. There is no truthiness: an empty string is not false, zero is not false, nil is not false. if x where x is an integer does not compile, which removes a whole family of bugs and costs you the occasional != 0.

string is an immutable sequence of bytes, conventionally UTF-8. Chapter 7 takes strings apart properly; until then, treat them as values you can compare with ==, concatenate with +, and index for bytes.

2.2 Declarations and Zero Values

Four ways to bring a name into being, in rough order of how often you will write them.

x := 42                 // short declaration, type inferred: int
var y int               // zero value: 0
var z int = 42          // explicit type and value
var a, b = 1, "two"     // several at once, types inferred

The short form only works inside a function. At package level, use var or const.

A variable declared without a value is not undefined; it holds the zero value for its type. Numbers are 0, strings are "", booleans are false, and pointers, slices, maps, channels, functions, and interfaces are nil. Go’s designers made the zero value useful on purpose, so a var buf bytes.Buffer is an empty buffer ready to write to and a var mu sync.Mutex is an unlocked mutex, neither needing a constructor. Watch for the type whose zero value is not useful, the map, covered in section 7.3.

2.3 Constants

A const is fixed at compile time and can be a number, string, or boolean. Constants that do not name a type are untyped, and this is more useful than it sounds: an untyped constant carries arbitrary precision and takes on whatever type the context needs.

const big = 1 << 100    // fine, no type yet
fmt.Println(big >> 98)  // 4: becomes an int here

1 << 100 cannot be stored in any Go integer, but as an untyped constant it does not have to be, and only the final expression has to fit. This is why you can write math.Pi * r * r with r a float32 and nothing complains.

iota is a counter that resets to zero at each const block and increments per line. Go writes enumerations and sequences with it.

// Constant generators.
package main

import "fmt"

type Weekday int

const (
	Sunday Weekday = iota
	Monday
	Tuesday
	Wednesday
	Thursday
	Friday
	Saturday
)

const (
	_  = iota             // skip 1
	KB = 1 << (10 * iota) // 1 << 10
	MB                    // 1 << 20
	GB                    // 1 << 30
)

func main() {
	fmt.Println(Sunday, Wednesday, Saturday)
	fmt.Println(KB, MB, GB)
}
0 3 6
1024 1048576 1073741824

A line in a const block with no expression repeats the previous one, so MB and GB need not restate the shift. Section 6.4 gives Weekday a String method so it prints as Sunday rather than 0.

2.4 Conversions

Go converts nothing implicitly. Mixing an int and a float64 in an expression is an error, not a promotion:

mismatch/main.go:8:14: invalid operation: n * f (mismatched types int and float64)

Every conversion is written T(v), and every one is visible at the point it happens. Three of them lose information quietly, so know them:

// Conversions, and three of them that surprise people.
package main

import (
	"fmt"
	"math"
)

func main() {
	i := 300
	fmt.Println(float64(i) / 7) // 42.857142857142854
	fmt.Println(byte(i))        // 44: 300 does not fit in a byte

	var big int32 = math.MaxInt32
	fmt.Println(big + 1) // -2147483648: wraps, silently

	fmt.Println(string(rune(65))) // "A"
	fmt.Println(fmt.Sprint(65))   // "65"
}

Converting to a narrower integer keeps the low bits and discards the rest, so byte(300) is 44 with no complaint. Signed integer overflow wraps rather than trapping. That is at least defined behavior, where C leaves it undefined, but it is still a wrong answer arriving silently. And string(rune(65)) is "A" because it converts a code point to the character it names, while turning the number 65 into the text "65" is strconv.Itoa or fmt.Sprint. Writing string(65) directly is a mistake common enough that go vet reports it.

Integer division truncates toward zero and the remainder takes the sign of the dividend, so -7/2 is -3 and -7%2 is -1. Dividing an integer by zero panics and stops the program; dividing a float by zero gives +Inf, -Inf, or NaN, following IEEE 754 without complaint.

2.5 Operators

Arithmetic is + - * / %, with % on integers only. Comparison is `== != < <=

=and yields abool. Logic is && || !, and &&and||` short-circuit.

Bitwise operators are & and, | or, ^ xor, &^ and-not, and << and >> for shifts. Unary ^ is bitwise complement, where C uses ~. The &^ operator clears bits, so x &^ mask is C’s x & ~mask in one token.

There is no operator overloading. + on two of your own struct types is an error, always, and a package that wants addition gives you an Add method. This is a real limitation for anyone writing numeric code and a real gift to anyone reading unfamiliar code, since a + b cannot be a function call in disguise.

== compares values, not references. Two structs are equal when their fields are equal, and a struct is comparable when all its fields are:

fmt.Println(Point{1, 2} == Point{1, 2}) // true

Slices, maps, and functions are not comparable at all and may only be tested against nil. When you need to compare two slices, slices.Equal does it.

2.6 Named Types

type gives a name to a type, and the result is a new type rather than an abbreviation. This matters more than it looks.

// A named type is a new type, not an alias.
package main

import "fmt"

type Celsius float64
type Fahrenheit float64

func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) }

func main() {
	boiling := Celsius(100)
	fmt.Printf("%g°C is %g°F\n", float64(boiling), float64(CToF(boiling)))
}

Celsius and Fahrenheit are both float64 underneath, and neither will assign to the other or to a plain float64 without a conversion. So a function taking a Celsius cannot be handed a temperature in the wrong scale, and the compiler enforces a distinction that a comment would only request. This costs you the conversions you can see in the Printf line, and buys you a class of bug that cannot happen.

You can convert between two named types with the same underlying type freely, because the conversion is what the reader needs to see. If you genuinely want a second name for the same type rather than a new one, that is type Celsius = float64, an alias, and it is rare outside of migrations.

Named types are also where methods come from, since a method must be declared on a type defined in the same package. Chapter 6.

Exercise 2-1. Print the largest and smallest values of every integer type without using the math constants, by shifting and subtracting.

Exercise 2-2. Write type Meters float64 and type Feet float64 with conversions both ways, then try to add a Meters to a Feet and read the error.

Exercise 2-3. Find the smallest positive integer that float64 cannot represent exactly. Confirm it by adding 1 and comparing.

3. Control Flow

Go has if, for, switch, and goto. That is the list. There is no while, no do, no unless, no ternary ?:, and no comprehension. Chapter 1 showed the shapes; this chapter fills them in and adds the parts that are actually different from what you know.

Two rules hold everywhere. Conditions take no parentheses and braces are never optional, not even around a one-line body. The second rule exists because Go’s semicolon insertion requires the opening brace on the same line as the if, and once the compiler is placing your braces there is nothing to argue about.

3.1 If

if x > 0 {
	fmt.Println("positive")
} else if x < 0 {
	fmt.Println("negative")
} else {
	fmt.Println("zero")
}

The condition must be a bool. Nothing else is accepted, so if n and if s and if p do not compile and must say n != 0, s != "", p != nil.

An if may run a short statement before its condition, with the names it declares scoped to the if and its else:

if v, ok := m[key]; ok {
	fmt.Println(v)
}

This is the shape you will write most often in real Go, because the two-value returns of Chapter 4 land straight into it. Keeping v and ok out of the surrounding scope is the point: they are answers to one question and they stop existing when it is answered.

Since a function that fails returns the error as a value, Go code is full of this:

f, err := os.Open(path)
if err != nil {
	return err
}

You will write that thousands of times. Chapter 8 is about what goes in the middle.

3.2 For

The three-clause form, the condition-only form, the bare infinite form, and range, all introduced in section 1.4. Four details remain.

break and continue take a label, letting you leave an outer loop from inside an inner one. A label is a name followed by a colon on the line before the loop.

// A label lets break and continue name which loop they mean.
package main

import "fmt"

func main() {
	const limit = 30

outer:
	for n := 2; n <= limit; n++ {
		for d := 2; d*d <= n; d++ {
			if n%d == 0 {
				continue outer
			}
		}
		fmt.Print(n, " ")
	}
	fmt.Println()
}
2 3 5 7 11 13 17 19 23 29

continue outer abandons the current n and moves to the next one. Without the label it would only abandon the current divisor, and every number would print. Labels replace the flag variable you would otherwise introduce and check at the top of the outer loop, and they are rare enough that seeing one is a signal to read carefully.

range copies. for _, v := range xs gives you a copy of each element, so assigning to v changes nothing. To modify the slice, index it: for i := range xs { xs[i] *= 2 }. The range expression itself is evaluated once, before the loop, so appending to a slice inside a loop that ranges over it does not extend the loop.

Each iteration gets a fresh loop variable.

// Each iteration gets its own copy of the loop variable.
package main

import "fmt"

func main() {
	var fns []func()
	for _, s := range []string{"a", "b", "c"} {
		fns = append(fns, func() { fmt.Print(s, " ") })
	}
	for _, f := range fns {
		f()
	}
	fmt.Println()
}

This prints a b c. Every closure captured a different s, because Go declares a new one per iteration. Before Go 1.22 there was one variable reused across the whole loop and the same program printed c c c, which was the single most reported bug in the language. If you find older code that copies the loop variable with s := s on the first line of the body, that is what it was working around, and it is now harmless and unnecessary.

Ranging over a function is how Go spells a custom iterator, added in 1.23. You will meet these as maps.Keys and slices.Values long before you write one, and section 7.5 shows the consuming side.

3.3 Switch

Go’s switch is the one place where the language is more generous than C rather than less.

// Switch on a value, with several matches per case.
package main

import "fmt"

func classify(r rune) string {
	switch r {
	case 'a', 'e', 'i', 'o', 'u':
		return "vowel"
	case ' ', '\t', '\n':
		return "space"
	}
	switch {
	case r >= '0' && r <= '9':
		return "digit"
	case r >= 'a' && r <= 'z':
		return "consonant"
	}
	return "other"
}

func main() {
	for _, r := range "go 1.23!" {
		fmt.Printf("%q %s\n", r, classify(r))
	}
}
'g' consonant
'o' vowel
' ' space
'1' digit
'.' other
'2' digit
'3' digit
'!' other

Cases do not fall through. The body of a matching case runs and the switch ends, with no break needed, which removes the most common C bug in the construct. When you want fall-through, the keyword fallthrough as the last statement in a case does it, and you will use it about once a year.

A case can list several values, separated by commas, as the vowels do above.

Cases need not be constants. They are evaluated in order and compared against the switch value, so case r >= '0' is legal in a switch on r.

The value is optional. A switch with nothing after the keyword tests true, so each case is an independent condition, and the construct becomes a tidier if-else chain:

// A switch with no value tested acts as an if-else chain.
package main

import "fmt"

func main() {
	for i := 1; i <= 15; i++ {
		switch {
		case i%15 == 0:
			fmt.Println("FizzBuzz")
		case i%3 == 0:
			fmt.Println("Fizz")
		case i%5 == 0:
			fmt.Println("Buzz")
		default:
			fmt.Println(i)
		}
	}
}

Prefer that form over if/else if whenever there are more than two branches. It lines the conditions up in a column where a reader can see them all at once, and adding a branch does not touch the others.

A switch also accepts a short statement first, like if: switch x := f(); {. And there is a second kind, the type switch, which asks what concrete type is inside an interface value; it needs Chapter 9 and appears there.

3.4 Goto

Go has goto and you will not use it. It jumps to a label in the same function and may not jump over a variable declaration or into a block. Generated code and the occasional deeply nested parser loop are where it actually appears.

The one thing you might reach for it for, unwinding after a failure partway through a function, has a better answer in defer, section 4.4.

Exercise 3-1. Rewrite classify so that the whole thing is one tagless switch. Decide which version you would rather read in six months.

Exercise 3-2. Write a program that reads lines from standard input and prints only those that are palindromes, ignoring case and spaces.

Exercise 3-3. The primes program tests divisors up to the square root. Add a counter for how many divisions it performs, and compare it against a version that tests every divisor up to n-1.

4. Functions

A Go function declares its parameters and results, and both are part of its type. Functions are values, so they can be stored, passed, and returned. There are no default arguments, no keyword arguments, and no overloading: one name, one function, one signature.

4.1 Declarations

func power(base, n int) int
func bounds(xs []int) (lo, hi int, ok bool)
func log(format string, args ...any)
func run()

Consecutive parameters of the same type name it once. A single result needs no parentheses; two or more do. A function with no results omits the type entirely.

There is no forward declaration and no header file. A function may call one declared later in the file, or in another file of the same package, because the compiler reads the whole package before it checks anything.

Arguments are passed by value, always. The function gets a copy, and assigning to a parameter changes nothing the caller can see. When the argument is a slice, map, or pointer the copy refers to the same underlying data, so the function can modify what it points at; Chapter 5 is about that distinction and it is the one people get wrong.

4.2 Multiple Results

// Returning more than one value.
package main

import "fmt"

// bounds returns the smallest and largest of xs, and whether there were any.
func bounds(xs []int) (lo, hi int, ok bool) {
	if len(xs) == 0 {
		return 0, 0, false
	}
	lo, hi = xs[0], xs[0]
	for _, x := range xs[1:] {
		lo, hi = min(lo, x), max(hi, x)
	}
	return lo, hi, true
}

func main() {
	if lo, hi, ok := bounds([]int{7, 2, 9, 4}); ok {
		fmt.Println(lo, hi)
	}
	_, _, ok := bounds(nil)
	fmt.Println("empty slice ok:", ok)
}
2 9
empty slice ok: false

Returning several values is ordinary in Go rather than a trick, and it is why the language needs neither output parameters nor a tuple type. Two conventions have hardened around it. A function that can fail returns its result and an error last, the subject of Chapter 8. A function that looks something up returns the value and a bool saying whether it was found, called the comma-ok form, which you also get from a map index, a channel receive, and a type assertion.

The results in bounds are named, which does two things: it documents which is which in the signature, where a caller reading (int, int, bool) would have to guess, and it declares those variables at the top of the function with their zero values. A bare return then returns whatever they currently hold. That last part is a trap in any function long enough that you cannot see the named results and the return at once, so name results for documentation and write the values out anyway, as bounds does.

min and max are built into the language and work on any ordered type.

4.3 Variadic Functions

// A variadic parameter collects the rest of the arguments into a slice.
package main

import "fmt"

func sum(label string, xs ...int) int {
	total := 0
	for _, x := range xs {
		total += x
	}
	fmt.Printf("%s: %v -> ", label, xs)
	return total
}

func main() {
	fmt.Println(sum("three", 1, 2, 3))
	fmt.Println(sum("none"))

	xs := []int{4, 5, 6}
	fmt.Println(sum("spread", xs...))
}
three: [1 2 3] -> 6
none: [] -> 0
spread: [4 5 6] -> 15

...int as the final parameter collects the remaining arguments into a []int. Inside the function it is an ordinary slice, empty rather than nil when no arguments were passed. To pass a slice you already have, append ... to it at the call site; without that, you would be passing one argument that happens to be a slice, and the compiler will say so.

This is how fmt.Println and friends accept anything, with ...any as the parameter. Chapter 9 explains what any is.

4.4 Defer

defer schedules a function call to run when the surrounding function returns, whether it returns normally, returns early, or panics.

f, err := os.Open(path)
if err != nil {
	return err
}
defer f.Close()

The close sits two lines from the open, so a reader checking whether the file is closed does not have to read to the end of the function, and adding an early return later cannot leak it. Every acquire-and-release pair in Go is written this way: unlock a mutex, close a file, stop a timer, restore a setting.

Three rules govern it.

// When deferred calls run, and what they see.
package main

import "fmt"

// order shows that deferred calls run last-in-first-out, and that their
// arguments are evaluated where the defer is written, not where it runs.
func order() {
	i := 0
	defer fmt.Println("first deferred, i was", i)
	i = 1
	defer fmt.Println("second deferred, i was", i)
	i = 2
	fmt.Println("body ends, i is", i)
}

// double uses a named result, which a deferred function can still change.
func double(n int) (result int) {
	defer func() { result *= 2 }()
	return n + 1
}

func main() {
	order()
	fmt.Println("double(3) =", double(3))
}
body ends, i is 2
second deferred, i was 1
first deferred, i was 0
double(3) = 8

Deferred calls run last-in-first-out, so releases happen in the reverse order of the acquisitions, as you want when they nest.

Arguments are evaluated where the defer is written, not where it runs. i was 0 when the first defer was reached and 0 is what it printed, even though i is 2 by the time the call happens. To defer a decision until the end instead, defer a closure, as double does.

A deferred closure can modify named results. double returns n + 1, which is assigned to result, and then the deferred function doubles it, so double(3) is 8 rather than 4. This is how a function catches a panic and turns it into an error return, and outside of that it is a good way to confuse somebody.

The one place defer bites is inside a loop. It fires at the end of the function, not the end of the iteration, so opening a thousand files in a loop with defer f.Close() holds a thousand of them open. Move the body into its own function when that happens.

4.5 Function Values and Closures

Functions are values with types. func(int) int is a type like any other, and a variable of that type can hold any function with that signature. Its zero value is nil, and calling a nil function panics.

// A function value carries the variables it refers to.
package main

import "fmt"

// counter returns a function that yields 1, 2, 3, ... on successive calls.
func counter() func() int {
	n := 0
	return func() int {
		n++
		return n
	}
}

func main() {
	next := counter()
	fmt.Println(next(), next(), next())

	other := counter()
	fmt.Println(other(), next())
}
1 2 3
1 4

counter returns an anonymous function that refers to n, a local variable of a function that has already returned. The variable survives because the closure refers to it, and each call to counter makes a new one, so other starts again at 1 while next carries on. Closures capture variables, not values, so the two functions returned by two calls share nothing.

An anonymous function called immediately is written func() { ... }(), and the trailing parentheses are what distinguish that from defining one.

4.6 Panic and Recover

panic stops the ordinary flow of a program: deferred calls run, the stack unwinds, and the program prints the panic message and a stack trace and exits. The runtime panics on your behalf for a bug that cannot be continued through, an index out of range or a nil dereference among them.

recover, called inside a deferred function, stops the unwinding and returns the value passed to panic.

defer func() {
	if r := recover(); r != nil {
		err = fmt.Errorf("recovered: %v", r)
	}
}()

Use this sparingly. Panics are for bugs, and errors are for the things that go wrong in a correct program, so a library that panics where it should return an error has moved a decision from the caller to itself. The reasonable uses are a package boundary where an internal recursive parser panics its way out of a malformed input, and a server that would rather lose one request than the whole process. Chapter 8 is about the other ninety-nine percent.

Exercise 4-1. Write func stats(xs []float64) (mean, stddev float64, ok bool).

Exercise 4-2. Write a function that takes a func(int) int and an int, and returns a function applying the first n times.

Exercise 4-3. Predict what this prints, then run it:

for i := range 3 {
	defer fmt.Print(i, " ")
}

Exercise 4-4. Write trace(name string) func() so that defer trace("work")() prints an entry message immediately and an exit message with the elapsed time when the function returns. Note where the parentheses go and why.

5. Pointers and Values

This is the chapter that changes how you read Go. In a scripting language the question “is this a copy or the same object?” usually has one answer for the whole language and you stop asking it. In Go it has a different answer per type, the answer is visible in the code once you know where to look, and getting it wrong produces a program that runs and is subtly incorrect.

The rule underneath is short: assignment copies the value, and the value is whatever the type says it is. Everything below is that rule applied.

5.1 What Copying Means

// What gets copied when you assign, and what does not.
package main

import "fmt"

type Point struct{ X, Y int }

func main() {
	// Structs copy.
	a := Point{1, 2}
	b := a
	b.X = 99
	fmt.Println(a, b) // {1 2} {99 2}

	// Arrays copy, including their elements.
	xs := [3]int{1, 2, 3}
	ys := xs
	ys[0] = 99
	fmt.Println(xs, ys) // [1 2 3] [99 2 3]

	// Slices copy the header, which points at shared elements.
	ss := []int{1, 2, 3}
	ts := ss
	ts[0] = 99
	fmt.Println(ss, ts) // [99 2 3] [99 2 3]
}
{1 2} {99 2}
[1 2 3] [99 2 3]
[99 2 3] [99 2 3]

A struct is its fields, so copying one copies every field, and b is a separate Point. An array is its elements, so copying a [3]int copies three integers, and ys is a separate array. Neither of these is how most languages behave and both follow from the rule.

A slice is not its elements. A slice value is a small header — a pointer to an array, a length, and a capacity — so copying a slice copies the header and leaves both copies pointing at the same elements. ts[0] = 99 therefore changes what ss sees. The slice was copied faithfully; the copy just happens to contain a pointer.

Maps and channels work the same way, and so, of course, do pointers. When someone calls these “reference types” that is what they mean: not that Go treats them specially, but that the value being copied has an address inside it. There is no special case in the language, so you can reason about it.

5.2 Pointers

&x is the address of x. *p is the value at p. The type of &x when x is an int is *int.

// Taking an address, following it, and passing one to a function.
package main

import "fmt"

type Counter struct{ N int }

// byValue gets a copy and cannot change the caller's Counter.
func byValue(c Counter) { c.N++ }

// byPointer gets an address and can.
func byPointer(c *Counter) { c.N++ }

func swap(a, b *int) { *a, *b = *b, *a }

func main() {
	c := Counter{}
	byValue(c)
	fmt.Println(c.N) // 0
	byPointer(&c)
	fmt.Println(c.N) // 1

	x, y := 1, 2
	swap(&x, &y)
	fmt.Println(x, y) // 2 1

	p := &Counter{N: 10} // address of a composite literal
	q := new(Counter)    // pointer to a zeroed Counter
	fmt.Println(p.N, q.N, *p, *q)
}
0
1
2 1
10 0 {10} {0}

byValue receives a copy of the Counter and increments the copy, which the caller never sees. byPointer receives the address and increments what it points at. Go has no call by reference; passing a pointer is the only way to let a function reach the caller’s value.

Notice c.N++ inside byPointer, where c is a *Counter. Go dereferences pointers to structs automatically for field access, so c.N means (*c).N and nobody writes the longer form. This convenience is why Go code has far fewer visible * than C.

&Counter{N: 10} takes the address of a composite literal, idiomatic in Go and with no C equivalent. new(T) allocates a zeroed T and returns a *T; it is the same thing as &T{} and you will see both.

What Go does not have is pointer arithmetic. You cannot add to a pointer, walk one through an array, or cast one type of pointer to another. A pointer either points at a valid object of its type or is nil. That is what lets the garbage collector work and what rules out the family of buffer-overflow bugs. (The unsafe package can do these things. It is for people writing runtimes and it is not part of this book.)

5.3 Nil

The zero value of every pointer is nil, and following one stops the program.

// Following a nil pointer stops the program.
package main

import "fmt"

type Counter struct{ N int }

func main() {
	defer func() {
		fmt.Println("recovered:", recover())
	}()

	var p *Counter
	fmt.Println("p is nil:", p == nil)
	fmt.Println(p.N)
}
p is nil: true
recovered: runtime error: invalid memory address or nil pointer dereference

Without the recover, that is a crash with a stack trace. Coming from Lux this is the change that deserves attention: Lux has no null, and Go’s most common runtime failure is the one Lux removed. Go’s answer is not a type-system guarantee but a set of habits — return an error alongside a pointer rather than a nil pointer meaning failure, make the zero value of your types useful so nobody needs a pointer to express “not set yet”, and check at the boundary where a value enters your code rather than at every use. Chapter 8 is where those habits get written down.

nil is also the zero value of slices, maps, channels, functions, and interfaces, and it means something slightly different for each. A nil slice behaves like an empty one for len, range, and append; a nil map reads like an empty one and panics on write; a nil channel blocks forever. Those are in Chapters 7 and 11.

5.4 When To Use a Pointer

Three reasons, and no others worth the trouble.

The function must modify the caller’s value. byPointer above. If a function’s job is to change something, it takes a pointer to it.

The value must not be copied. A sync.Mutex protects the struct it lives in, and a copy of the struct has a second, independent mutex protecting nothing. Types like this are documented as “must not be copied”, and go vet enforces it for the ones it knows:

vet/copylock/main.go:13:13: show passes lock by value: goexamples/errors/vet/copylock.Registry contains sync.Mutex
vet/copylock/main.go:17:7: call of show copies lock value: goexamples/errors/vet/copylock.Registry contains sync.Mutex

The value is large. Copying a struct with thirty fields on every call costs something. In practice this reason is invoked far more often than it is measured; a struct of a handful of words copies faster than the pointer chase that avoiding the copy would cost, so let this one wait for a profiler.

The reason not to use a pointer is that a value cannot be nil, so a function taking a value has one less thing to check and one less way to fail. Prefer values, and reach for a pointer when one of the three reasons applies.

5.5 Where Values Live

You never free anything. Go is garbage collected, and a value stays alive as long as something refers to it.

You also do not choose between the stack and the heap. The compiler decides, by asking whether a value outlives the function that made it, and that analysis is why return &Counter{} is safe in Go where the equivalent in C returns a dangling pointer.

// Whether a value outlives its function decides where the compiler puts it.
package main

import "fmt"

type Big struct{ a [64]int }

func escapes() *Big { return &Big{} }

func stays() Big { return Big{} }

func main() {
	fmt.Println(len(escapes().a), len(stays().a))
}

go build -gcflags=-m prints the reasoning:

$ go build -gcflags=-m ./ch05/escape
ch05/escape/main.go:8:30: &Big{} escapes to heap

The Big made inside escapes outlives the call, so it goes on the heap; the one stays returns by value does not, so it does not. Read this output when you are chasing an allocation and ignore it otherwise. The practical consequence is only that taking the address of a local variable is always safe, so you can write the clear thing and let the compiler place it.

Exercise 5-1. Write func zero(xs []int) that sets every element to 0, and func zeroArray(xs [5]int) that tries to do the same. Call both and explain the difference without using the word “reference”.

Exercise 5-2. Write a linked list of integers with type node struct { val int; next *node }, plus functions to push at the front and to print the list.

Exercise 5-3. Write a function that takes a *int and returns without touching it, then call it with nil. Then write one that dereferences it. Note which of the two the compiler complains about.

6. Structs and Methods

A struct groups named fields into one value. A method is a function attached to a type. Between them they cover the ground that classes cover in other languages, and they do it with two mechanisms instead of one, the trade Go makes throughout.

6.1 Structs

// Struct literals, methods, and comparison.
package main

import (
	"fmt"
	"math"
)

type Point struct {
	X, Y float64
}

// Add returns the sum of p and q. The receiver p is a copy.
func (p Point) Add(q Point) Point { return Point{p.X + q.X, p.Y + q.Y} }

// Dist returns the distance from p to the origin.
func (p Point) Dist() float64 { return math.Hypot(p.X, p.Y) }

func main() {
	a := Point{3, 4}       // positional
	b := Point{X: 1, Y: 1} // by name
	var c Point            // zero value
	fmt.Println(a, b, c)   // {3 4} {1 1} {0 0}
	fmt.Println(a.Add(b))  // {4 5}
	fmt.Printf("%.1f\n", a.Dist())
	fmt.Println(a == Point{3, 4}) // structs compare field by field
}
{3 4} {1 1} {0 0}
{4 5}
5.0
true

A composite literal fills the fields. The positional form Point{3, 4} requires every field in declaration order, and it breaks silently in meaning — though not in compilation — the day somebody reorders the struct, so keep it for small types whose field order is part of their definition, like a point, and use Point{X: 1, Y: 1} everywhere else. The named form may also omit fields, which then take their zero values.

var c Point gives you a Point with both fields zero. There is no constructor to call and no allocation to think about, and Go’s convention is to make that zero value mean something sensible so callers can skip the ceremony.

Structs are comparable with == when all their fields are, comparing field by field, and usable as map keys on the same condition. A struct containing a slice is not comparable.

Fields whose names begin with a capital letter are visible outside the package and the rest are not, the same rule as everywhere else in Go. That is also what determines which fields encoding/json will marshal, and the backtick string after a field, the struct tag, is how you tell it the name to use:

type User struct {
	Name  string `json:"name"`
	Email string `json:"email,omitempty"`
}

An anonymous struct declares a type and a value at once, and is the right tool for a one-off grouping that deserves no name. Table-driven tests are built out of them, and Chapter 10 shows that.

cases := []struct {
	in   string
	want int
}{
	{"", 0},
	{"a b", 2},
}

6.2 Methods

A method is a function with a receiver, declared between func and the name.

func (p Point) Dist() float64 { return math.Hypot(p.X, p.Y) }

Inside, p is an ordinary parameter that happens to be written first. The receiver’s type must be a named type declared in the same package, which stops you adding methods to int or to somebody else’s type, and it need not be a struct:

// A String method changes how fmt prints a value.
package main

import "fmt"

type Weekday int

const (
	Sunday Weekday = iota
	Monday
	Tuesday
	Wednesday
	Thursday
	Friday
	Saturday
)

var names = [...]string{"Sunday", "Monday", "Tuesday", "Wednesday",
	"Thursday", "Friday", "Saturday"}

func (d Weekday) String() string {
	if d < Sunday || d > Saturday {
		return fmt.Sprintf("Weekday(%d)", int(d))
	}
	return names[d]
}

func (d Weekday) IsWeekend() bool { return d == Sunday || d == Saturday }

func main() {
	fmt.Println(Wednesday, Saturday, Weekday(9))
	fmt.Println(Wednesday.IsWeekend(), Saturday.IsWeekend())
}
Wednesday Saturday Weekday(9)
false true

Weekday is an int with methods. The String method is special by convention rather than by language rule: fmt looks for it on any value it is asked to print, so defining one changes how the type appears everywhere, including inside slices, maps, and structs that contain it. Chapter 9 names the interface it satisfies. Note that String handles a value outside the valid range instead of panicking on the array index, because a String method that panics turns a debugging session into a worse debugging session.

6.3 Pointer Receivers

// A value receiver gets a copy; a pointer receiver gets the original.
package main

import "fmt"

type Counter struct{ N int }

func (c Counter) IncValue()    { c.N++ } // modifies the copy
func (c *Counter) IncPointer() { c.N++ } // modifies the caller's Counter

func main() {
	c := Counter{}
	c.IncValue()
	c.IncValue()
	fmt.Println(c.N) // 0

	c.IncPointer() // shorthand for (&c).IncPointer()
	c.IncPointer()
	fmt.Println(c.N) // 2
}
0
2

A value receiver gets a copy, so IncValue increments something that is thrown away when it returns. A pointer receiver gets the address, so IncPointer increments the caller’s Counter. This is section 5.2 with different punctuation, and it produces the same class of quiet bug.

c.IncPointer() is shorthand for (&c).IncPointer(), which Go inserts because c is addressable. The reverse also works: given a *Counter, calling a value-receiver method dereferences automatically. So the calls look identical at the call site and only the declaration tells you which you have.

The rule for choosing:

Use a pointer receiver when the method modifies the receiver, or when the type contains a sync.Mutex or anything else that must not be copied, or when the struct is genuinely large.

Be consistent within a type. If any method needs a pointer receiver, give them all pointer receivers, even the ones that only read. Mixing them means readers have to check each method, and it interacts badly with interfaces: Chapter 9 shows that a Counter value does not satisfy an interface whose methods have pointer receivers, while a *Counter satisfies both kinds.

Since the zero value should be useful, Go has no constructors. When a type needs setup, write a function named New or NewThing returning the value or a pointer to it, and that is a convention with no support from the language.

6.4 Embedding

Declaring a field with a type and no name embeds it. The outer type gets the inner type’s fields and methods as if they were its own.

// Embedding promotes the inner type's fields and methods to the outer one.
package main

import "fmt"

type Animal struct {
	Name string
	Legs int
}

func (a Animal) Describe() string {
	return fmt.Sprintf("%s has %d legs", a.Name, a.Legs)
}

type Dog struct {
	Animal // embedded: no field name
	Breed  string
}

// Dog overrides Describe by declaring its own.
func (d Dog) Describe() string {
	return d.Animal.Describe() + ", and is a " + d.Breed
}

func main() {
	d := Dog{Animal{"Rex", 4}, "collie"}
	fmt.Println(d.Name)              // promoted field
	fmt.Println(d.Legs)              // promoted field
	fmt.Println(d.Animal.Describe()) // the inner method
	fmt.Println(d.Describe())        // the outer one
}
Rex
4
Rex has 4 legs
Rex has 4 legs, and is a collie

d.Name works because Name was promoted from the embedded Animal, and so was Describe until Dog declared its own, which shadows it. The inner one is still reachable by its full path, d.Animal.Describe(), and that is how Dog builds on it. Promotion stops at the first match, so a name declared on the outer type always wins.

This looks like inheritance and it is not. Here is the difference:

// Embedding is not inheritance: the inner type never calls the outer one.
package main

import "fmt"

type Animal struct{ Name string }

func (a Animal) Speak() string { return "..." }
func (a Animal) Greet() string { return a.Name + " says " + a.Speak() }

type Dog struct{ Animal }

func (d Dog) Speak() string { return "woof" }

func main() {
	d := Dog{Animal{"Rex"}}
	fmt.Println(d.Speak()) // woof
	fmt.Println(d.Greet()) // Rex says ... -- not "woof"
}
woof
Rex says ...

Greet is declared on Animal, so its receiver is an Animal, and the a.Speak() inside it calls Animal.Speak. It cannot call Dog.Speak, because by the time Greet runs there is no Dog in sight — only the embedded Animal that was copied into the call. There is no virtual dispatch and no super. An embedded type does not know it has been embedded.

If you came here expecting a base class to call an overridden method, that is the thing Go declines to provide, and the answer is an interface: define the behavior as an interface, have Greet take one, and pass the Dog. Chapter 9.

What embedding does buy you is composition without writing forwarding methods by hand. Embedding a sync.Mutex gives your type Lock and Unlock methods directly. Embedding an interface in a struct gives you a type that satisfies it while overriding one method. And embedding one interface in another is how io.ReadWriter is built out of io.Reader and io.Writer.

Exercise 6-1. Give Point a Scale(f float64) method that multiplies both fields in place. Decide which receiver it needs and check by calling it.

Exercise 6-2. Write type Temperature float64 with a String method printing one decimal and a degree sign. Put one in a struct and print the struct.

Exercise 6-3. Write a Stack of ints with Push, Pop and Len. Pop should use the comma-ok form. Make the zero value a usable empty stack.

Exercise 6-4. Add a Speak method to Animal in the last program that Greet calls, then try to make d.Greet() print woof without changing Greet’s receiver type. Convince yourself it cannot be done.

7. Slices, Maps, and Strings

Three types carry nearly all the data in a Go program. None of them is complicated, and each has one behavior that will surprise you once.

7.1 Arrays

An array’s length is part of its type. [3]int and [4]int are different types, a function taking one will not accept the other, and the length must be a constant. Arrays are values, so assigning one copies every element, as section 5.1 showed.

var a [3]int              // three zeros
b := [3]string{"x", "y"}  // third is ""
c := [...]int{1, 2, 3, 4} // length 4, counted by the compiler

You will rarely declare one. Arrays exist so that slices can be built on them, and they appear directly in the places where a fixed size is the point, like a [32]byte hash.

7.2 Slices

A slice is a view into an array: a pointer to an element, a length, and a capacity. []int is a slice of ints, with no length in the type.

// Length, capacity, and what append does when it runs out.
package main

import "fmt"

func main() {
	var s []int // nil, but usable
	fmt.Println(s == nil, len(s), cap(s))

	for i := range 5 {
		s = append(s, i)
		fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
	}

	t := make([]int, 3, 10) // length 3, capacity 10
	fmt.Println(len(t), cap(t), t)

	u := s[1:3] // elements 1 and 2
	fmt.Println(u, len(u), cap(u))

	v := make([]int, len(s))
	copy(v, s) // an independent copy
	v[0] = 99
	fmt.Println(s, v)
}
true 0 0
len=1 cap=1 [0]
len=2 cap=2 [0 1]
len=3 cap=4 [0 1 2]
len=4 cap=4 [0 1 2 3]
len=5 cap=8 [0 1 2 3 4]
3 10 [0 0 0]
[1 2] 2 7
[0 1 2 3 4] [99 1 2 3 4]

The zero value of a slice is nil, and a nil slice is a working empty slice: len is 0, range runs zero times, and append works. You almost never need to initialize a slice before appending to it, and you should compare against len(s) == 0 rather than s == nil, because an empty non-nil slice is a different value that behaves identically.

append returns a new slice and you must use the result. When the existing capacity has room, it writes into the array already there and hands back a longer view. When it does not, it allocates a bigger array, copies, and hands back a slice of that. The growth above went 1, 2, 4, 4, 8; the exact schedule is not specified and has changed between releases, so rely on it being amortized constant time and not on any particular number.

make([]T, len, cap) allocates a slice of a given length with room to grow. When you know roughly how many elements are coming, make([]int, 0, n) and then appending avoids every intermediate copy, and it is the one slice optimization worth doing by reflex.

s[i:j] takes elements i up to but not including j, either index omissible. The result shares storage with s, so writing through one is visible through the other, and its capacity runs to the end of the underlying array, so s[1:3] above reports a capacity of 7. This is also a way to leak: a small slice of a large array keeps the whole array alive. copy(dst, src) copies the overlap of the two and returns how many elements moved, and it is how you get an independent copy.

Now the surprise.

// append may or may not share storage with the slice it was given.
package main

import "fmt"

func main() {
	base := make([]int, 3, 8) // room to spare
	a := append(base, 10)
	b := append(base, 20) // reuses the same array as a
	fmt.Println(a, b)     // both end in 20

	full := make([]int, 3) // no room to spare
	c := append(full, 10)
	d := append(full, 20) // has to allocate, so c is untouched
	fmt.Println(c, d)
}
[0 0 0 20] [0 0 0 20]
[0 0 0 10] [0 0 0 20]

a and b were appended from the same base slice. Because base had spare capacity, both appends wrote to the same array position, and b’s write clobbered a’s. In the second pair there was no spare capacity, so each append allocated and the results are independent. The behavior of append therefore depends on a capacity you may not have thought about, and the same code is correct or wrong depending on how the slice was made.

The rule that keeps you out of it: treat the slice you passed to append as invalid afterwards. Write s = append(s, x) and never keep the old value around. When you genuinely need to append to a shared base twice, cut the capacity with the three-index form, base[:3:3], which forces the allocation.

The slices package has the operations you would otherwise write by hand: slices.Sort, slices.Contains, slices.Index, slices.Equal, slices.Reverse, slices.Max, slices.SortFunc. Use them.

7.3 Maps

A map is a hash table from a comparable key type to any value type.

// Maps: creation, the comma-ok form, deletion, and ordered iteration.
package main

import (
	"fmt"
	"maps"
	"slices"
	"strings"
)

func main() {
	counts := map[string]int{}
	for _, w := range strings.Fields("the quick the lazy the end") {
		counts[w]++ // missing keys read as the zero value
	}

	n, ok := counts["the"]
	fmt.Println(n, ok)
	m, ok := counts["missing"]
	fmt.Println(m, ok)

	delete(counts, "lazy")
	fmt.Println(len(counts))

	// Iteration order is randomized, so sort the keys when order matters.
	for _, k := range slices.Sorted(maps.Keys(counts)) {
		fmt.Printf("%s=%d ", k, counts[k])
	}
	fmt.Println()

	var nilMap map[string]int
	fmt.Println(nilMap["anything"], len(nilMap)) // reads are fine
}
3 true
0 false
3
end=1 quick=1 the=3
0 0

counts[w]++ works on a key that is not there, because reading a missing key gives the zero value rather than an error. That makes counters and accumulators short, and it means an index expression cannot tell you whether the key exists. The comma-ok form does: v, ok := m[k] sets ok to whether the key was present. Use it whenever zero is a legitimate stored value.

delete(m, k) removes a key and does nothing if it was not there. len(m) counts the entries. clear(m) empties it.

Iteration order is randomized, deliberately and on every run, so that nobody writes code depending on an order the implementation does not promise. When you need a stable order, sort the keys, which slices.Sorted(maps.Keys(m)) does in one line.

The nil map is the one type whose zero value is not fully usable. Reading from it and taking its length work, and writing to it panics:

// Reading a nil map is fine. Writing to one is not.
package main

import "fmt"

func main() {
	var m map[string]int
	fmt.Println(m["absent"], len(m)) // 0 0
	m["absent"] = 1                  // panics
}
0 0
panic: assignment to entry in nil map

So a map field in a struct needs a make(map[K]V) before first write, while a slice field does not. That asymmetry catches everyone once.

Map elements are not addressable — you cannot write &m[k] or m[k].field = v — because a rehash can move them. When the value is a struct you want to modify, either store a pointer, map[string]*Thing, or read the struct out, change it, and put it back.

7.4 Strings

A string is an immutable sequence of bytes. It is conventionally UTF-8 but the language does not enforce that; a string can hold any bytes at all.

// A string is bytes; ranging over it yields runes.
package main

import (
	"fmt"
	"unicode/utf8"
)

func main() {
	s := "héllo"

	fmt.Println(len(s), utf8.RuneCountInString(s))
	fmt.Println(s[1]) // a byte, not a character

	for i, r := range s {
		fmt.Printf("%d:%q ", i, r)
	}
	fmt.Println()

	r := []rune(s)
	fmt.Println(len(r), string(r[1]), string(r[4:]))
	fmt.Println([]byte(s)[:3])
}
6 5
195
0:'h' 1:'é' 3:'l' 4:'l' 5:'o'
5 é o
[104 195 169]

len(s) is a count of bytes, and s[i] is the i-th byte, of type byte. "héllo" is six bytes and five characters because é takes two, so len says 6 and s[1] is 195, the first of those two bytes rather than a character.

Ranging over a string decodes UTF-8 as it goes and gives you the byte index and the rune at it. That is why the indices above are 0, 1, 3, 4, 5 and skip 2. A malformed byte yields utf8.RuneError and advances by one, so the loop always terminates.

Three conversions, all of which allocate and copy. []byte(s) gives the bytes. []rune(s) decodes into a slice of code points, for when you need to index or slice by character. string(bs) and string(rs) go back. utf8.RuneCountInString(s) counts characters without allocating.

Strings are immutable, so s[0] = 'H' does not compile, and s += t in a loop allocates a new string every time. For anything more than a few concatenations use strings.Builder:

// Building a string without quadratic copying.
package main

import (
	"fmt"
	"strings"
)

func main() {
	var b strings.Builder
	for i := range 5 {
		fmt.Fprintf(&b, "%d,", i)
	}
	fmt.Println(strings.TrimSuffix(b.String(), ","))

	fmt.Println(strings.Join([]string{"a", "b", "c"}, "-"))
	fmt.Println(strings.Repeat("ab", 3))
	fmt.Println(strings.ToUpper("go"), strings.Contains("seafood", "foo"))
	fmt.Println(strings.Split("a,b,,c", ","))
	fmt.Printf("%q\n", strings.Fields("  spaced   out  "))
}
0,1,2,3,4
a-b-c
ababab
GO true
[a b  c]
["spaced" "out"]

A strings.Builder is an io.Writer, so fmt.Fprintf writes to it, the neatest way to assemble formatted output. Its zero value is ready to use.

The strings package is worth ten minutes with go doc strings. Note the two splitters that differ: strings.Split("a,b,,c", ",") keeps the empty field and returns four elements, while strings.Fields splits on runs of whitespace and returns none. strings.Cut is the modern way to split on the first separator.

7.5 Iterators

Since Go 1.23, range accepts a function of the right shape. That is how the standard library hands back sequences without building a slice. You saw one above: maps.Keys(m) yields keys, and slices.Sorted collects a sequence into a sorted slice. slices.Values, slices.All, and maps.All are the others you will meet first.

Writing your own means returning a func(yield func(T) bool), and you can go a long way without ever doing it. Consume them now; write them when a type of yours has a natural sequence and building a slice for it would be wasteful.

Exercise 7-1. Write func unique(xs []int) []int preserving first-seen order. Do it with a map for membership and a slice for order.

Exercise 7-2. Write a word-frequency program: read standard input, count words case-insensitively, and print the ten most common with their counts. slices.SortFunc will do the ranking.

Exercise 7-3. Write func reverse(s string) string that works on "héllo". Then check what it does to a string containing an emoji, and find out what a combining character is.

Exercise 7-4. Predict the output of append on a slice made with make([]int, 0, 4) versus one made with []int{}, then run it.

8. Errors

Go has no exceptions. A function that can fail says so in its signature by returning an error alongside its result, the caller checks it on the next line, and control flow stays where you can see it. This is the part of Go that people complain about most and change their minds about latest.

8.1 Error Is an Interface

type error interface {
	Error() string
}

That is the definition, copied from the standard library. Any type with an Error() string method is an error, which means errors are ordinary values: you can store them, compare them, put them in a slice, return them from a closure, and define your own.

The convention is that error is the last result and a nil error means success. Every function in the standard library follows it, and so should yours.

f, err := os.Open(path)
if err != nil {
	return err
}
defer f.Close()

Three lines per call is a real cost and Go pays it deliberately. The trade is that a reader can see every place a function can fail without knowing which calls throw and which do not, and there is no invisible jump out of the middle of a function. Whether that is worth it is an argument you can have; how to write it well is not, and the rest of this chapter is that.

8.2 Making Errors

errors.New builds one from a string. fmt.Errorf formats one, and its %w verb wraps another error inside the new one.

// A sentinel error, a wrapped error, and the two ways to inspect one.
package main

import (
	"errors"
	"fmt"
	"strconv"
)

// ErrNegative is returned for an age below zero. Callers compare against it
// with errors.Is rather than by string.
var ErrNegative = errors.New("age must not be negative")

func parseAge(s string) (int, error) {
	n, err := strconv.Atoi(s)
	if err != nil {
		return 0, fmt.Errorf("parsing age %q: %w", s, err)
	}
	if n < 0 {
		return 0, fmt.Errorf("parsing age %q: %w", s, ErrNegative)
	}
	return n, nil
}

func main() {
	for _, in := range []string{"42", "-1", "old"} {
		n, err := parseAge(in)
		switch {
		case err == nil:
			fmt.Println("ok:", n)
		case errors.Is(err, ErrNegative):
			fmt.Println("negative:", err)
		default:
			var numErr *strconv.NumError
			if errors.As(err, &numErr) {
				fmt.Printf("not a number: %v (input %q)\n", err, numErr.Num)
			}
		}
	}
}
ok: 42
negative: parsing age "-1": age must not be negative
not a number: parsing age "old": strconv.Atoi: parsing "old": invalid syntax (input "old")

Add context as the error travels up. Each layer prefixes what it was doing, so parsing age "old": strconv.Atoi: parsing "old": invalid syntax tells you the whole path without a stack trace. Write the message lower-case and without punctuation at the end, because it will be embedded in someone else’s sentence. And do not include the word “error” or “failed”; the reader knows.

Use %w when the caller might reasonably want to inspect the underlying error and %v when it is an implementation detail you would rather not promise. %w makes the wrapped error part of your API.

8.3 Sentinels and Types

Two ways to let a caller ask what went wrong, and one way that does not work.

A sentinel is a package-level error value that callers compare against, like ErrNegative above or io.EOF, os.ErrNotExist, and sql.ErrNoRows in the standard library. Compare with errors.Is, never with ==, because == fails the moment somebody wraps it. errors.Is unwraps as it goes.

A custom type carries structured detail that a message cannot.

// An error type carrying structured detail.
package main

import (
	"errors"
	"fmt"
	"strings"
)

type ParseError struct {
	Line int
	Text string
}

func (e *ParseError) Error() string {
	return fmt.Sprintf("line %d: cannot parse %q", e.Line, e.Text)
}

func parse(input string) (map[string]string, error) {
	out := map[string]string{}
	for i, line := range strings.Split(input, "\n") {
		if line == "" {
			continue
		}
		k, v, ok := strings.Cut(line, "=")
		if !ok {
			return nil, fmt.Errorf("config: %w", &ParseError{Line: i + 1, Text: line})
		}
		out[k] = v
	}
	return out, nil
}

func main() {
	_, err := parse("a=1\nbroken\nc=3")
	fmt.Println(err)

	var pe *ParseError
	if errors.As(err, &pe) {
		fmt.Println("the bad line number was", pe.Line)
	}
}
config: line 2: cannot parse "broken"
the bad line number was 2

errors.As looks down the chain for an error of the type you asked for and assigns it to your variable, so the caller can reach pe.Line. Note the pointer receiver on Error(), which means *ParseError is the error type and ParseError is not, and that errors.As takes &pe, a pointer to the pointer.

The way that does not work is strings.Contains(err.Error(), "not found"). Error strings are for humans and change without notice.

When several things are wrong at once, errors.Join combines them into one error whose message lists them and which errors.Is can still search:

// Reporting every problem instead of only the first.
package main

import (
	"errors"
	"fmt"
)

type User struct {
	Name  string
	Email string
	Age   int
}

func validate(u User) error {
	var errs []error
	if u.Name == "" {
		errs = append(errs, errors.New("name is empty"))
	}
	if u.Email == "" {
		errs = append(errs, errors.New("email is empty"))
	}
	if u.Age < 0 {
		errs = append(errs, errors.New("age is negative"))
	}
	return errors.Join(errs...) // nil when errs is empty
}

func main() {
	fmt.Println(validate(User{Name: "Ada", Email: "ada@example.com", Age: 36}))
	if err := validate(User{Age: -1}); err != nil {
		fmt.Println("invalid:")
		fmt.Println(err)
	}
}
<nil>
invalid:
name is empty
email is empty
age is negative

errors.Join returns nil when every argument is nil, so the accumulate-then-join shape works without a length check.

8.4 The Typed Nil

This is Go’s most famous bug and it is worth ten minutes of your attention now rather than an afternoon later.

// A nil pointer inside a non-nil interface. Go's most famous error bug.
package main

import "fmt"

type ParseError struct{ Line int }

func (e *ParseError) Error() string { return fmt.Sprintf("line %d", e.Line) }

// broken returns a typed nil, which is not a nil error.
func broken() error {
	var e *ParseError // nil
	return e          // wrapped in an interface: not nil
}

// fixed returns the untyped nil.
func fixed() error {
	return nil
}

func main() {
	fmt.Println(broken() == nil) // false
	fmt.Println(fixed() == nil)  // true

	if err := broken(); err != nil {
		fmt.Printf("caller thinks it failed: %v (%T)\n", err, err)
	}
}
false
true
caller thinks it failed: <nil> (*main.ParseError)

An interface value holds two things, a type and a value, and it is nil only when both are. broken returns a *ParseError that is nil, but the interface it gets stored in records the type *ParseError, so the interface is not nil and err != nil is true. The caller reports a failure that did not happen.

Avoid it with one rule: declare error variables as error, not as a concrete pointer type. If broken had written var e error and assigned to it only on the failing path, there would be nothing to go wrong. The bug arrives when somebody declares var e *ParseError at the top of a function so they can fill in fields, then returns it whether or not it was ever used.

8.5 Panic

panic is for a bug in your program, not for a condition your program should handle. An index out of range, a nil dereference, an impossible switch default: these mean the code is wrong, and continuing would do damage. Everything else — a missing file, bad input, a refused connection — is an error value, because it is a thing that happens to a correct program.

The test to apply: could a caller reasonably recover from this? If yes, it is an error. A library that panics on bad input has taken that choice away from everyone who imports it.

recover belongs at a boundary you control, and there are two of them worth the trouble. A recursive parser can panic its way out of a malformed input and recover at the top-level entry point, turning the panic back into an error before it escapes the package. And a server can recover per request, so that one bad handler loses one response rather than every connection in flight.

8.6 Errors at the Top

main is where errors stop. The pattern is to keep main short and put the work in a function that returns an error:

func main() {
	if err := run(); err != nil {
		fmt.Fprintln(os.Stderr, "myprog:", err)
		os.Exit(1)
	}
}

Diagnostics go to standard error, prefixed with the program name, so a user piping your output gets data on one stream and complaints on the other. os.Exit with a non-zero status is how the shell finds out. Note that os.Exit does not run deferred functions, the reason to call it here rather than deep in the program. Chapter 10 builds a program around this shape.

Exercise 8-1. Write func readConfig(path string) (map[string]string, error) that wraps both the file error and any parse error with the path.

Exercise 8-2. Give ParseError a Column field and an Unwrap method returning an underlying cause, then check that errors.Is finds a sentinel through two layers.

Exercise 8-3. Write a function returning (*bytes.Buffer, error) that demonstrates the typed-nil bug, then fix it two ways: by changing the declaration, and by returning an explicit nil on the success path.

Exercise 8-4. Read the io.EOF documentation and explain why errors.Is(err, io.EOF) and err == io.EOF differ for a wrapped reader.

9. Interfaces and Generics

An interface is a set of method signatures. A type satisfies it by having those methods, and says nothing about it. That one decision — satisfaction is structural, checked by the compiler, never declared — is what makes Go’s abstraction feel different from every language with an implements keyword.

9.1 Interfaces

// A type satisfies an interface by having its methods. Nothing is declared.
package main

import (
	"fmt"
	"math"
)

type Shape interface {
	Area() float64
	Perimeter() float64
}

type Rect struct{ W, H float64 }

func (r Rect) Area() float64      { return r.W * r.H }
func (r Rect) Perimeter() float64 { return 2 * (r.W + r.H) }

type Circle struct{ R float64 }

func (c Circle) Area() float64      { return math.Pi * c.R * c.R }
func (c Circle) Perimeter() float64 { return 2 * math.Pi * c.R }

func describe(s Shape) string {
	return fmt.Sprintf("%T area=%.2f perimeter=%.2f", s, s.Area(), s.Perimeter())
}

func main() {
	for _, s := range []Shape{Rect{3, 4}, Circle{1}} {
		fmt.Println(describe(s))
	}
}
main.Rect area=12.00 perimeter=14.00
main.Circle area=3.14 perimeter=6.28

Rect and Circle never mention Shape. They have the two methods, so they are Shapes, and a third type in another package written by somebody who never heard of yours is a Shape too if it has them. Interfaces can therefore be defined after the types that satisfy them, and by the package that needs them rather than the package that provides them.

That leads to the piece of Go design advice worth internalizing early: define an interface where it is used, not where it is implemented, and keep it small. A function that only reads should take an io.Reader, not your *FileStore, and the interface should list the one method the function calls. Then any caller can pass anything, including a strings.Reader in a test, without either side having planned for it.

The standard library’s most-used interfaces have one method each:

type Reader interface{ Read(p []byte) (n int, err error) }
type Writer interface{ Write(p []byte) (n int, err error) }
type Stringer interface{ String() string }
// One interface, one method, and a type of your own that satisfies it.
package main

import (
	"bytes"
	"fmt"
	"io"
	"os"
	"strings"
)

// report writes to anything with a Write method.
func report(w io.Writer, name string) {
	fmt.Fprintf(w, "report for %s\n", name)
}

// countingWriter counts the bytes passing through it and forwards them.
type countingWriter struct {
	inner io.Writer
	n     int
}

func (c *countingWriter) Write(p []byte) (int, error) {
	c.n += len(p)
	return c.inner.Write(p)
}

func main() {
	var buf bytes.Buffer
	var sb strings.Builder

	report(&buf, "a byte buffer")
	report(&sb, "a string builder")
	report(os.Stdout, "standard output")

	fmt.Print(buf.String(), sb.String())

	// A wrapper is a writer too, so it drops in anywhere one is expected.
	c := &countingWriter{inner: os.Stdout}
	report(c, "a counting wrapper")
	fmt.Println("bytes written:", c.n)
}
report for standard output
report for a byte buffer
report for a string builder
report for a counting wrapper
bytes written: 30

Files, network connections, HTTP response bodies, gzip compressors, hash functions, and strings.Builder are all io.Writers, so anything written against that one method works with all of them. countingWriter shows the other half: because the interface is small, wrapping one is a few lines, and the wrapper is itself a writer.

Interfaces embed: io.ReadWriter is declared as interface { Reader; Writer } and nothing more.

9.2 Interface Values

An interface value holds a dynamic type and a value. %T prints the first, so the shapes above reported main.Rect. An interface is nil only when both halves are empty, the bug in section 8.4.

A type assertion extracts the concrete value back out. s.(Circle) panics if the dynamic type is not Circle; c, ok := s.(Circle) reports it instead and is what you should write. Asserting to an interface type instead asks whether the value also satisfies that one. That is how code discovers optional behavior: if wt, ok := w.(io.WriterTo); ok takes a fast path when the writer has one.

A type switch handles several possibilities at once, and it is where Go sends you when you want a sum type.

9.3 Rebuilding a Sum Type

Lux gives you an enum with associated values and a match the compiler forces you to complete. Go has neither. What you build instead is an interface with an unexported method, which no package but yours can implement, and a type switch.

// What a Lux enum becomes in Go, and what you give up.
package main

import "fmt"

// Shape is closed to other packages: the unexported method cannot be
// implemented outside this one.
type Shape interface {
	isShape()
}

type Circle struct{ R float64 }
type Square struct{ Side float64 }
type Tri struct{ A, B, C float64 }

func (Circle) isShape() {}
func (Square) isShape() {}
func (Tri) isShape()    {}

func name(s Shape) string {
	switch v := s.(type) {
	case Circle:
		return fmt.Sprintf("circle r=%g", v.R)
	case Square:
		return fmt.Sprintf("square s=%g", v.Side)
	default:
		// Nothing made us handle Tri. This is the case Lux would have caught.
		return fmt.Sprintf("unhandled %T", v)
	}
}

func main() {
	for _, s := range []Shape{Circle{1}, Square{2}, Tri{3, 4, 5}} {
		fmt.Println(name(s))
	}

	// A type assertion asks for one specific type.
	var s Shape = Circle{7}
	if c, ok := s.(Circle); ok {
		fmt.Println("radius", c.R)
	}
	if _, ok := s.(Square); !ok {
		fmt.Println("not a square")
	}
}
circle r=1
square s=2
unhandled main.Tri
radius 7
not a square

isShape() is a marker. It does nothing, and because it is unexported no type outside this package can have it, so the set of Shapes is closed and a reader can find all of them by searching one file. That much translates.

What does not translate is the guarantee. name handles Circle and Square and silently does the wrong thing for Tri, and the compiler is content: default covers it. Lux would have refused to build. This is the concrete cost of the different bet described in the preface, and the mitigations are habits rather than checks. Put the type switch and the type declarations in the same file. Make default panic with the unhandled type during development rather than returning something plausible. And when the set is large or changing, add a linter that checks type switches for completeness, since the compiler will not.

9.4 any

any is an alias for interface{}, the interface with no methods, which every type satisfies. It is the parameter type of fmt.Println and the value type of a heterogeneous map.

Reach for it rarely. A value in an any has had its type erased as far as the compiler is concerned, and getting it back means an assertion that can fail at run time, so a function taking any has given up the checking you came to Go for. Before generics it was the only tool for writing something that worked on many types. Now it usually is not.

9.5 Generics

A function can take type parameters, written in square brackets before the value parameters.

// Type parameters: one function, many types.
package main

import (
	"fmt"
	"strings"
)

// Map applies f to every element, returning a new slice.
func Map[T, U any](xs []T, f func(T) U) []U {
	out := make([]U, 0, len(xs))
	for _, x := range xs {
		out = append(out, f(x))
	}
	return out
}

// Number constrains T to the types that + works on.
type Number interface {
	~int | ~int64 | ~float64
}

func Sum[T Number](xs []T) T {
	var total T
	for _, x := range xs {
		total += x
	}
	return total
}

type Celsius float64 // ~float64 admits named types too

func main() {
	fmt.Println(Map([]int{1, 2, 3}, func(n int) string {
		return strings.Repeat("*", n)
	}))
	fmt.Println(Sum([]int{1, 2, 3}))
	fmt.Println(Sum([]float64{1.5, 2.5}))
	fmt.Println(Sum([]Celsius{10, 20}))
}
[* ** ***]
6
4
30

Map[T, U any] works on a slice of any type and produces a slice of any other. The call sites pass no type arguments because the compiler infers them from the values, as usual.

A constraint is an interface used as a bound on a type parameter. any allows everything and therefore permits almost nothing, so Sum cannot use it: + is not defined on a type parameter bounded by any. Number is a constraint listing the permitted underlying types, and the ~ means “any type whose underlying type is this”, so Celsius is admitted. comparable is built in and allows ==, as a map key parameter needs. cmp.Ordered in the standard library covers every type that < works on, so most constraints you need are already written.

When to use generics. When you would otherwise write the same function once per type, as the slices and maps packages do. Not when an interface with a method would do the job — a generic function knows nothing about T beyond its constraint, so if the behavior differs per type you want a method, not a type parameter. The Go proverb is that a little duplication is cheaper than the wrong abstraction, and generics have not repealed it.

Methods cannot have their own type parameters. A type can be generic, so type Stack[T any] struct { items []T } works and its methods use the type’s parameters, but you cannot add a parameter on the method itself.

Exercise 9-1. Write a Shape implementation in a second file for a right triangle and confirm you did not have to modify anything.

Exercise 9-2. Write an io.Writer that discards everything and counts calls. Compare it against io.Discard.

Exercise 9-3. Add Tri to the type switch in sumtype, then add a fourth shape and do not add it to the switch. Note that nothing tells you.

Exercise 9-4. Write func Filter[T any](xs []T, keep func(T) bool) []T and func Keys[K comparable, V any](m map[K]V) []K. Then find the standard-library functions that already do both.

10. Building a Program

Everything so far has been one file. This chapter is about the rest: how code is split into packages, how packages are grouped into a module, how you test it, and how the go command ties it together. Then it builds a real command-line program that uses all of it.

10.1 Packages

One directory is one package. Every .go file in a directory must declare the same package name, and a package cannot span directories. The package name is conventionally the last element of the directory path, lower case, one word, no underscores.

A package is the unit of visibility. Capitalized names are exported to importers and everything else is private to the package, including across files, so two files in the same package see each other’s unexported names freely. This is why Go packages tend to be a handful of files rather than one file per type.

Import paths are the module path plus the directory. Inside the module goexamples, the package in ch10/wf/count is imported as goexamples/ch10/wf/count and referred to as count:

import "goexamples/ch10/wf/count"

counts, err := count.Words(r)

Name packages for what they provide, and remember the name will be read at every call site. count.Words reads well; utils.CountWords does not, and a package called util, common, helpers, or base is a sign that nobody decided what it was for.

A directory named internal is special to the toolchain: packages under it can only be imported by code rooted at its parent. That is how you publish a module with a private implementation.

10.2 Modules

A module is a tree of packages versioned together, defined by a go.mod at its root:

module goexamples

go 1.23

go mod init <path> writes it. The module path is what importers will type, so for anything you publish it is the repository URL, like github.com/anderix/wf. Dependencies are added by go get, which records them in go.mod, and go mod tidy adds anything imported and missing while removing anything recorded and unused. Run it before you commit.

go.sum records a checksum for every dependency and must be committed. It is what makes a build reproducible and what would notice if a version’s contents ever changed.

Versions are git tags of the form v1.4.2, and Go takes semantic versioning literally: a major version above 1 goes in the import path, so v2 of a module is imported as github.com/anderix/wf/v2. That is blunt, and it means an incompatible change cannot silently break anybody.

10.3 A Program

wf prints the most frequent words in its input. It is two packages: a library that does the work and a main that deals with the command line.

ch10/wf/
    main.go              package main
    count/
        count.go         package count
        count_test.go

The library knows nothing about files, flags, or standard output.

// Package count tallies word frequencies in a stream of text.
package count

import (
	"bufio"
	"cmp"
	"io"
	"slices"
	"strings"
	"unicode"
)

// Pair is one word and how often it occurred.
type Pair struct {
	Word string
	N    int
}

// Words reads r to the end and returns a count of the words in it. A word is a
// run of letters, digits, and apostrophes; everything else separates. Words are
// lowercased so that "The" and "the" count together.
func Words(r io.Reader) (map[string]int, error) {
	counts := map[string]int{}

	in := bufio.NewScanner(r)
	in.Split(bufio.ScanWords)
	for in.Scan() {
		w := strings.ToLower(strings.TrimFunc(in.Text(), isNotWord))
		if w != "" {
			counts[w]++
		}
	}
	return counts, in.Err()
}

func isNotWord(r rune) bool {
	return !unicode.IsLetter(r) && !unicode.IsDigit(r) && r != '\''
}

// Top returns the n most frequent words, most frequent first, with ties broken
// alphabetically so the result is stable. A n of zero or less returns all of
// them.
func Top(counts map[string]int, n int) []Pair {
	pairs := make([]Pair, 0, len(counts))
	for w, c := range counts {
		pairs = append(pairs, Pair{w, c})
	}

	slices.SortFunc(pairs, func(a, b Pair) int {
		if d := cmp.Compare(b.N, a.N); d != 0 {
			return d
		}
		return cmp.Compare(a.Word, b.Word)
	})

	if n > 0 && n < len(pairs) {
		pairs = pairs[:n]
	}
	return pairs
}

Words takes an io.Reader rather than a filename, section 9.1’s advice in practice: the caller can pass a file, a network connection, or a strings.Reader in a test. Top returns a sorted slice rather than printing, so the decision about output belongs to whoever called it. Both are exported and documented; isNotWord is neither.

Sorting deserves a note. slices.SortFunc takes a comparison returning a negative number, zero, or a positive one, and cmp.Compare produces those. Comparing b.N to a.N rather than the other way round is what puts the largest first, and falling through to compare the words makes the order total, so the output does not shuffle between runs on ties.

Now the command:

// Command wf prints the most frequent words in its input.
//
// Usage:
//
//	wf [-n count] [file...]
//
// With no files, wf reads standard input.
package main

import (
	"flag"
	"fmt"
	"io"
	"os"

	"goexamples/ch10/wf/count"
)

func main() {
	if err := run(); err != nil {
		fmt.Fprintln(os.Stderr, "wf:", err)
		os.Exit(1)
	}
}

func run() error {
	n := flag.Int("n", 10, "how many words to print")
	flag.Parse()

	readers, closeAll, err := open(flag.Args())
	if err != nil {
		return err
	}
	defer closeAll()

	counts, err := count.Words(io.MultiReader(readers...))
	if err != nil {
		return err
	}

	for _, p := range count.Top(counts, *n) {
		fmt.Printf("%7d %s\n", p.N, p.Word)
	}
	return nil
}

// open turns the named files into readers, or returns standard input when
// there are none. The returned function closes whatever was opened.
func open(names []string) ([]io.Reader, func(), error) {
	if len(names) == 0 {
		return []io.Reader{os.Stdin}, func() {}, nil
	}

	var readers []io.Reader
	var files []*os.File
	closeAll := func() {
		for _, f := range files {
			f.Close()
		}
	}

	for _, name := range names {
		f, err := os.Open(name)
		if err != nil {
			closeAll()
			return nil, nil, err
		}
		files = append(files, f)
		readers = append(readers, f)
	}
	return readers, closeAll, nil
}
$ go run ./ch10/wf -n 5 gettysburg.txt
      3 and
      3 nation
      2 a
      2 are
      2 conceived

Four things in that file are the standard shape of a Go program.

main does nothing but call run and report. run returns an error, main prints it to standard error with the program’s name and exits non-zero. Keeping main to four lines means everything else is testable and everything else can use defer, which os.Exit would skip.

Flags come from the flag package. flag.Int returns a *int that is filled in by flag.Parse, and the remaining arguments are flag.Args(). You get -n 5, -n=5, and a generated -h for free.

Files are opened through a helper that also returns its own cleanup, so run can defer closeAll() without knowing how many files there were, and a failure partway through closes the ones already opened.

io.MultiReader concatenates readers, so several files become one stream and count.Words never learns there was more than one. Composing small interfaces like this is what the standard library is for.

$ go run ./ch10/wf nope.txt
wf: open nope.txt: no such file or directory
$ echo $?
1

10.4 Testing

Tests live beside the code in files ending _test.go, which the compiler excludes from normal builds. A test is a function named TestXxx taking a *testing.T. There is no assertion library in the standard library and the convention is not to use one: you write the comparison yourself and call t.Errorf with a message saying what you did, what you got, and what you wanted.

package count

import (
	"fmt"
	"strings"
	"testing"
)

func TestWords(t *testing.T) {
	cases := []struct {
		name string
		in   string
		want map[string]int
	}{
		{"empty", "", map[string]int{}},
		{"case folded", "The the THE", map[string]int{"the": 3}},
		{"punctuation stripped", "hi, there. hi!", map[string]int{"hi": 2, "there": 1}},
		{"apostrophe kept", "don't don't", map[string]int{"don't": 2}},
		{"digits are words", "in 1863 in", map[string]int{"in": 2, "1863": 1}},
	}

	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			got, err := Words(strings.NewReader(c.in))
			if err != nil {
				t.Fatalf("Words(%q) returned error: %v", c.in, err)
			}
			if len(got) != len(c.want) {
				t.Fatalf("Words(%q) = %v, want %v", c.in, got, c.want)
			}
			for w, n := range c.want {
				if got[w] != n {
					t.Errorf("Words(%q)[%q] = %d, want %d", c.in, w, got[w], n)
				}
			}
		})
	}
}

func TestTopBreaksTiesAlphabetically(t *testing.T) {
	got := Top(map[string]int{"pear": 2, "apple": 2, "fig": 5}, 3)
	want := []Pair{{"fig", 5}, {"apple", 2}, {"pear", 2}}

	if len(got) != len(want) {
		t.Fatalf("Top() = %v, want %v", got, want)
	}
	for i := range want {
		if got[i] != want[i] {
			t.Errorf("Top()[%d] = %v, want %v", i, got[i], want[i])
		}
	}
}

func TestTopLimit(t *testing.T) {
	counts := map[string]int{"a": 3, "b": 2, "c": 1}
	if got := Top(counts, 2); len(got) != 2 {
		t.Errorf("Top(counts, 2) returned %d pairs, want 2", len(got))
	}
	if got := Top(counts, 0); len(got) != 3 {
		t.Errorf("Top(counts, 0) returned %d pairs, want all 3", len(got))
	}
}

func ExampleTop() {
	counts, _ := Words(strings.NewReader("go go go stop stop wait"))
	for _, p := range Top(counts, 2) {
		fmt.Println(p.N, p.Word)
	}
	// Output:
	// 3 go
	// 2 stop
}

func BenchmarkWords(b *testing.B) {
	text := strings.Repeat("the quick brown fox jumps over the lazy dog ", 100)
	b.ReportAllocs()
	for range b.N {
		Words(strings.NewReader(text))
	}
}
$ go test ./ch10/...
?   	goexamples/ch10/wf	[no test files]
ok  	goexamples/ch10/wf/count	0.002s

Table-driven tests are the Go idiom. A slice of anonymous structs holds the cases and one loop runs them, so adding a case is one line and the logic is written once. t.Run gives each case a name and its own subtest, so a failure points at the case and you can rerun one:

$ go test -run 'TestWords/case_folded' ./ch10/wf/count

t.Errorf records a failure and keeps going; t.Fatalf stops that subtest. Use Fatalf when continuing would panic or produce noise, as it does after an unexpected error, and Errorf otherwise so one run reports every problem.

A failure names the file, the line, and the subtest:

--- FAIL: TestWords (0.00s)
    --- FAIL: TestWords/case_folded (0.00s)
        count_test.go:29: Words("The the THE") = map[THE:1 The:1 the:1], want map[the:3]
FAIL

That is what dropping the strings.ToLower from Words produces, and the message tells you the input, the result, and the expectation without opening the test.

An Example function is a test and documentation at once. ExampleTop compiles, runs, and has its output compared against the // Output: comment, so a documented example cannot rot. It also appears in go doc output and on the generated documentation page, attached to Top because of its name.

A Benchmark function takes a *testing.B and runs the body b.N times, with the framework choosing b.N. It runs only when asked:

$ go test -bench=Words -benchtime=100x ./ch10/wf/count
BenchmarkWords-8   	     100	     48032 ns/op	    8432 B/op	     904 allocs/op

b.ReportAllocs adds the last two columns, which are usually more actionable than the timing. Go 1.24 added for b.Loop() as a replacement for for range b.N that is harder to get wrong; older code uses b.N and so does this book, which targets 1.23.

Four flags to know. -v lists every test as it runs. -run takes a regular expression selecting tests and subtests. -cover reports statement coverage, and -race builds with the race detector, Chapter 11’s subject and a thing that belongs in your CI.

A test that needs input files puts them in a directory named testdata, which the go tool ignores when building.

10.5 Documentation

A comment immediately above a declaration, starting with the declared name, is its documentation. go doc count.Words prints it; go doc -all count prints the package.

The comment above package documents the package itself, conventionally starting Package count ..., and for a main package it starts Command wf ... and describes the usage, as wf does. That convention is the reason go doc output reads consistently across the whole ecosystem.

Write the first sentence so it stands alone, because it is what appears in the package index next to the name.

10.6 The go Command

go run ./pkg          build and run
go build ./...        build everything, leaving binaries
go install ./cmd/wf   build and put the binary in ~/go/bin
go test ./...         run every test
go fmt ./...          format
go vet ./...          report likely mistakes the compiler allows
go doc strings.Cut    print documentation
go mod tidy           fix up dependencies
go clean -cache       when you suspect the build cache, which you will not

./... means this directory and everything under it, and it is how you address a whole project.

go vet deserves a habit. It catches Printf verbs that do not match their arguments, copied mutexes, unreachable code, and shadowed errors — real bugs that are legal Go. Run it alongside your tests, since go test already runs a subset of it.

Exercise 10-1. Add a -min flag to wf that ignores words shorter than some length. Test the library change, not the flag.

Exercise 10-2. Add a case to TestWords for input containing a hyphenated word and decide what the right answer is before you look at what the code does.

Exercise 10-3. Move count to internal/count and confirm the program still builds. Then try to import it from a package outside ch10/wf.

Exercise 10-4. Write a benchmark comparing Top on a map of 10 words against one of 10,000. Use b.Run to make it one benchmark with two sizes.

11. Concurrency

A goroutine is a function running independently of the one that started it. It costs a keyword and a few kilobytes of stack that grows as needed, so a Go program can have thousands where it would have a handful of threads. That cheapness is the point: structuring a program as concurrent pieces becomes an ordinary decision rather than an architectural commitment.

Concurrency is not parallelism. Concurrency is a way of writing a program as independent activities; parallelism is those activities running at the same instant on different cores. Go gives you the first and lets the runtime decide about the second.

11.1 Goroutines

go f(x) calls f(x) in a new goroutine and returns immediately. The result is discarded, so a goroutine communicates by writing somewhere the rest of the program can see.

// Starting goroutines and waiting for them to finish.
package main

import (
	"fmt"
	"strings"
	"sync"
)

func main() {
	words := []string{"alpha", "beta", "gamma", "delta"}
	upper := make([]string, len(words))

	var wg sync.WaitGroup
	for i, w := range words {
		wg.Add(1)
		go func() {
			defer wg.Done()
			upper[i] = strings.ToUpper(w) // each goroutine owns one slot
		}()
	}
	wg.Wait()

	fmt.Println(upper)
}
[ALPHA BETA GAMMA DELTA]

A sync.WaitGroup counts outstanding work. Add before starting, Done when finished — deferred, so a panic still counts — and Wait blocks until the count reaches zero. Without the Wait, main would return and the program would exit with the goroutines half-run; nothing waits for a goroutine on your behalf.

Each goroutine here writes upper[i] for its own i and reads nothing another one writes, so there is no sharing and no lock. Writing to distinct elements of a slice from different goroutines is safe. Appending to one slice from several is not.

This program also depends on the per-iteration loop variable from section 3.2. Before Go 1.22 every goroutine would have seen the last i and w.

11.2 Channels

A channel is a typed pipe. ch <- v sends, <-ch receives, and both block until the other side is ready, making a channel a synchronization device and not just a queue.

// Sending, receiving, closing, and ranging.
package main

import "fmt"

// squares sends the squares of 1..n and closes the channel when done.
func squares(n int) <-chan int {
	out := make(chan int)
	go func() {
		defer close(out)
		for i := 1; i <= n; i++ {
			out <- i * i
		}
	}()
	return out
}

func main() {
	// Ranging over a channel ends when the channel is closed.
	for v := range squares(5) {
		fmt.Print(v, " ")
	}
	fmt.Println()

	// A buffered channel accepts sends until the buffer is full.
	buf := make(chan string, 2)
	buf <- "one"
	buf <- "two"
	close(buf)
	fmt.Println(len(buf), cap(buf))

	// Receiving from a closed channel yields the zero value at once.
	// The comma-ok form tells you which happened.
	for {
		v, ok := <-buf
		if !ok {
			fmt.Println("closed")
			break
		}
		fmt.Println("got", v)
	}
}
1 4 9 16 25
2 2
got one
got two
closed

make(chan int) is unbuffered: a send waits for a receiver and a receive waits for a sender, so the two goroutines meet at the moment of handoff. make(chan T, n) is buffered: sends succeed without a receiver until the buffer is full. Use unbuffered by default, because the blocking is usually the behavior you want, and reach for a buffer when you know the producer should run ahead.

close(ch) says no more values are coming. Receiving from a closed channel returns the zero value immediately, and v, ok := <-ch reports which happened. Ranging over a channel receives until it is closed, so squares closes with a defer.

Two rules about closing. The sender closes, never the receiver, because only the sender knows there is no more. And closing is optional — it exists to signal receivers, so a channel nobody ranges over need never be closed. Sending on a closed channel panics; closing twice panics.

Note the return type <-chan int. A channel type can be direction-limited, and squares returns a receive-only view, so a caller cannot send to it or close it. Use these in signatures; they document the direction and the compiler enforces it.

11.3 Select

select waits on several channel operations and proceeds with whichever is ready first, choosing at random among those ready together.

// select waits on several channel operations at once.
package main

import (
	"fmt"
	"time"
)

func main() {
	ticks := time.Tick(10 * time.Millisecond)
	done := time.After(35 * time.Millisecond)

	n := 0
	for {
		select {
		case <-ticks:
			n++
		case <-done:
			fmt.Println("ticks before the deadline:", n)
			return
		}
	}
}
ticks before the deadline: 3

A default case makes a select non-blocking: it runs when nothing else is ready. That turns a receive into a poll and a send into a drop-if-full, both of which are occasionally what you want and usually a sign of a design that should be blocking instead.

time.After returns a channel that delivers once, and that is how you put a timeout on any blocking operation. A nil channel blocks forever, so setting a channel variable to nil disables its case in a loop’s select — the standard way to stop listening to one source without restructuring the loop.

11.4 A Worker Pool

The shape you will reach for most: fan work out to a fixed number of goroutines and collect the results.

// A worker pool: fan work out to N goroutines, collect the results.
package main

import (
	"fmt"
	"slices"
	"sync"
)

func main() {
	const workers = 4

	jobs := make(chan int)
	results := make(chan int)

	var wg sync.WaitGroup
	for range workers {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for n := range jobs {
				results <- n * n
			}
		}()
	}

	// Feed the jobs, then close so the workers' range loops end.
	go func() {
		defer close(jobs)
		for i := 1; i <= 10; i++ {
			jobs <- i
		}
	}()

	// Close results once every worker has stopped.
	go func() {
		wg.Wait()
		close(results)
	}()

	var got []int
	for r := range results {
		got = append(got, r)
	}

	slices.Sort(got) // arrival order is not defined
	fmt.Println(got)
}
[1 4 9 16 25 36 49 64 81 100]

Three goroutine roles and a closing rule for each. The workers range over jobs and stop when it is closed. The feeder closes jobs when it has sent everything. A separate goroutine waits for all the workers and then closes results, which it must, because no single worker knows when the last one has finished.

Getting that wrong is the standard concurrency bug in Go. Close results in a worker and the other workers panic sending to it; never close it and the collecting range blocks forever, a deadlock the runtime will report if nothing else is running:

fatal error: all goroutines are asleep - deadlock!

The results are sorted because arrival order depends on scheduling. If your output depends on which goroutine finished first, it is not deterministic, and a test asserting on it will pass on your machine and fail in CI.

Do not leak goroutines. A goroutine blocked on a channel nobody will ever use again never returns, and its stack is never collected. Every goroutine you start should have a clear reason it will end: a channel that gets closed, a context that gets canceled, or a loop with a bound.

11.5 Sharing Memory

Two goroutines touching the same variable, at least one of them writing, is a data race. It is not a slow path or a wrong ordering; it is undefined behavior, and the program may do anything.

// Two goroutines writing one variable. Run it with -race.
package main

import (
	"fmt"
	"sync"
)

func main() {
	var wg sync.WaitGroup
	counter := 0

	for range 1000 {
		wg.Add(1)
		go func() {
			defer wg.Done()
			counter++ // read, add, write: not atomic
		}()
	}
	wg.Wait()

	fmt.Println("counter:", counter)
}

Three runs of that program printed 860, 892, and 860. It should print 1000. counter++ is a read, an add, and a write, and goroutines interleave between them, so increments are lost.

Go ships a detector for this. go run -race and go test -race instrument memory access and report the conflict with both stacks:

WARNING: DATA RACE
Read at 0x00c00011c038 by goroutine 16:
  main.main.func1()
      ch11/race/main.go:17 +0x7b

Previous write at 0x00c00011c038 by goroutine 12:
  main.main.func1()
      ch11/race/main.go:17 +0x8d

It only reports races that actually occur during the run, so it finds nothing in code your tests do not exercise concurrently, but it has close to no false positives: if it reports a race, there is a race. Run your tests with -race in CI.

The fixes:

// The same counter, made safe two ways.
package main

import (
	"fmt"
	"sync"
	"sync/atomic"
)

func main() {
	var wg sync.WaitGroup

	var mu sync.Mutex
	guarded := 0

	var counted atomic.Int64

	for range 1000 {
		wg.Add(1)
		go func() {
			defer wg.Done()

			mu.Lock()
			guarded++
			mu.Unlock()

			counted.Add(1)
		}()
	}
	wg.Wait()

	fmt.Println(guarded, counted.Load())
}
1000 1000

sync.Mutex makes a section exclusive. Lock, touch the data, unlock — and defer mu.Unlock() when the section is longer than two lines or can return early. The mutex and the data it protects belong in the same struct, with the mutex declared immediately above them, so a reader can see the association. sync.RWMutex allows many concurrent readers and is worth it only when reads dominate and are slow.

The sync/atomic types handle a single value without a lock, and atomic.Int64 with its Add and Load methods is all most programs need.

Go’s advice is to prefer the channel: do not communicate by sharing memory; share memory by communicating. Give each piece of data one owner and pass it along a channel instead of locking it. That is genuinely better for pipelines and worker pools. It is worse for a shared counter or a cache, where a mutex is simpler and faster, and reaching for a channel to protect one integer is a mistake people make trying to be idiomatic.

11.6 Context

A context.Context carries a cancellation signal down a call tree, and it is how a Go program stops work it no longer needs.

ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()

select {
case res := <-work(ctx):
	use(res)
case <-ctx.Done():
	return ctx.Err() // context deadline exceeded
}

The convention is that a Context is the first parameter of any function that blocks, named ctx, and that it is passed down rather than stored in a struct. Every standard-library call that can wait accepts one, so a timeout at the top of a request propagates to the database query at the bottom without either layer knowing about the other.

Always call cancel, deferred, even when the operation finished. Not calling it leaks the context’s own goroutine and timer.

11.7 What To Reach For

Most concurrency in real programs is one of three shapes. A worker pool, as above, when you have many independent items and want a bounded number in flight. A pipeline of stages connected by channels, each stage a goroutine reading from one and writing to the next. And one goroutine per request in a server, which the net/http package already does for you, where your job is only to make what they share safe.

Reach for concurrency when the program waits on something — network, disk, other processes — or when the work is genuinely parallel and large enough to pay for the coordination. A concurrent version of a fast sequential loop is usually slower and always harder to read.

Exercise 11-1. Rewrite the goroutines program to send results on a channel instead of writing into a slice. Note what you have to do to keep the order.

Exercise 11-2. Add a -workers flag to a version of wf from Chapter 10 that counts several files concurrently, one goroutine per file, merging the maps at the end. Measure whether it is faster.

Exercise 11-3. Write a program that starts a goroutine blocked on a channel receive and then returns from main. Then make the same program deadlock, and read what the runtime prints.

Exercise 11-4. Take the race program and fix it with a channel instead of a mutex: one goroutine owns the counter and receives increment messages. Compare the two for length and for speed with -bench.

Appendix A: Reference Summary

A compact reminder of the language, not a specification. The Go Programming Language Specification at https://go.dev/ref/spec is short enough to read in an evening and is the authority.

Keywords

break     default      func    interface  select
case      defer        go      map        struct
chan      else         goto    package    switch
const     fallthrough  if      range      type
continue  for          import  return     var

Twenty-five, and that is the list. Everything else you see is a predeclared identifier, not a keyword, and can be shadowed.

Predeclared identifiers

Types      bool byte complex64 complex128 error float32 float64
           int int8 int16 int32 int64 rune string
           uint uint8 uint16 uint32 uint64 uintptr
           any comparable
Constants  true false iota
Zero       nil
Functions  append cap clear close complex copy delete imag len
           make max min new panic print println real recover

Declarations

var x int              zero value
var x = v              inferred type
var x int = v          both
x := v                 short form, inside functions only
const K = v            compile-time constant
type T U               new type with U's representation
type T = U             alias for U
func f(a, b int) error function
func (r T) m()         method on T

Zero values

0        numeric types
false    bool
""       string
nil      pointer, slice, map, channel, func, interface

A struct’s zero value has every field at its own zero value. An array’s has every element at its own.

Composite types

[N]T           array, length is part of the type
[]T            slice
map[K]V        map, K must be comparable
chan T         channel; <-chan T receive-only, chan<- T send-only
*T             pointer
func(A) B      function
struct{...}    struct
interface{...} interface
make([]T, len, cap)   make(map[K]V)   make(chan T, buf)
new(T)                &T{...}
T{a, b}               positional literal
T{Field: a}           keyed literal

Operators, tightest binding first

5   *  /  %  <<  >>  &  &^
4   +  -  |  ^
3   == != <  <= >  >=
2   &&
1   ||

Unary: + - ! ^ * & <-. There is no ?:, no **, no operator overloading, and ++ and -- are statements rather than expressions.

Control flow

if cond { } else if cond { } else { }
if init; cond { }

for init; cond; post { }
for cond { }
for { }
for i, v := range collection { }
for i := range n { }

switch v { case a, b: ... default: ... }
switch { case cond: ... }
switch v := x.(type) { case T: ... }

select { case <-ch: ... case ch <- v: ... default: ... }

break  continue  goto  fallthrough  return  defer  go

Labels attach to for, switch, and select, and break/continue may name one.

Format verbs

%v   default format          %+v  with struct field names
%#v  Go syntax               %T   the type
%d   integer                 %b %o %x %X  base 2, 8, 16
%f   decimal float           %e   exponent      %g  whichever is shorter
%s   string                  %q   quoted        %c  the rune
%t   bool                    %p   pointer       %%  a literal percent

Width and precision go between: %6.2f, %-10s left-justified, %08d zero-padded.

Frequently used packages

fmt          formatted I/O
os           files, arguments, environment, exit
strings      search, split, replace, Builder
strconv      text to and from numbers
bytes        the strings operations, on []byte
bufio        buffered I/O and Scanner
io           Reader, Writer, Copy, MultiReader
errors       New, Is, As, Join
slices       Sort, Contains, Index, Equal, SortFunc
maps         Keys, Values, Clone
sort         the older sorting interface
time         durations, instants, timers
sync         WaitGroup, Mutex, Once
context      cancellation and deadlines
encoding/json  Marshal and Unmarshal
net/http     client and server
testing      T, B, and the test runner
flag         command-line flags
math, math/rand, regexp, path/filepath, log

go doc <pkg> prints any of them.

The go command

go run ./pkg              build and run
go build ./...            build
go install ./cmd/x        build into ~/go/bin
go test ./...             test        -v -run -cover -race -bench
go fmt ./...              format
go vet ./...              report likely mistakes
go doc pkg.Name           documentation
go mod init <path>        start a module
go mod tidy               reconcile dependencies
go get pkg@version        add or update one
go clean -cache           discard the build cache

Naming

Capitalized names are exported from their package; the rest are not. Packages are one lower-case word. Acronyms keep their case, so HTTPServer and userID. Getters drop the get, so it is u.Name() rather than u.GetName(). A one-method interface is the method plus -er: Reader, Stringer, Formatter. Receiver names are one or two letters and the same on every method of a type. Short names are expected for short lives, so i, n, err, w, buf.

Appendix B: Coming From Another Language

From Lux

Lux was built to be outgrown, and Go is one of the three languages it was built to be outgrown into. The syntax is close enough that most Lux programs are a mechanical rewrite. The data model is not, and the differences are all in one direction: Lux was strict about a few things Go leaves to you.

LuxGo
let x = 5x := 5 — Go has no immutable local
var x = 5x := 5
int, float, string, boolint, float64, string, bool
[int][]int
[[int]][][]int
queue += 4queue = append(queue, 4)
length(xs)len(xs)
func f(n: int) -> int { }func f(n int) int { }
print("x is", x)fmt.Println("x is", x)
eprint(msg)fmt.Fprintln(os.Stderr, msg)
for x in xsfor _, x := range xs
for i in 0..5for i := range 5
while cfor c
if a && bif a && b
struct Point { x: int }type Point struct { X int }
Point(x: 3, y: 4)Point{X: 3, Y: 4}
enum Shape { circle(radius: float) }an interface plus one struct per case
match s { circle(let r) => ... }switch v := s.(type) { case Circle: ... }
Option<int>, some(x), none(int, bool), or *int and nil
Result<int, string>, ok(x), err(e)(int, error), nil, errors.New(e)
parseInt(s)strconv.Atoi(s), returning a value and an error
int(3.9), string(42)int(3.9), strconv.Itoa(42)
contains(s, sub)strings.Contains(s, sub)
readFile(path)os.ReadFile(path), returning bytes and an error
args()os.Args[1:]
run(cmd)exec.Command(...).Output()
func main() { }func main() { }, and it must be in package main

The mechanical differences are the type after the name with no colon, func without an arrow, capitalized names for anything another package should see, and braces on the same line as the thing they open. Fix those and a Lux program is close to Go.

Four differences are real.

There is no let. Every local variable in Go can be reassigned. const exists but only for compile-time constants, so it cannot hold the result of a function call. The habit Lux taught you — reach for the name that cannot change — has no direct spelling here, and what replaces it is scope: declare the variable as late as possible, in the smallest block that needs it, so there is less room for anything to reassign it.

There is nil. Lux removed null and Go put it back, and a nil pointer dereference is Go’s most common runtime failure. Chapter 5.3 covers the habits that stand in for the guarantee.

Enums become interfaces, and nothing checks your cases. lux convert go on an enum produces an interface with an unexported marker method and one struct per case, and turns match into a type switch. That is the right translation and it is what Chapter 9.3 teaches. What it cannot reproduce is exhaustiveness: Lux refuses to run a match that misses a case, and Go’s type switch quietly falls through to default. This is the single largest thing you are giving up, and it is better learned here than in a debugger.

Option becomes two values or a pointer. Lux’s some/none maps onto either the comma-ok pattern, v, ok := m[k], or a pointer that may be nil, which is what lux convert go emits. The comma-ok form is preferred in Go because it cannot be dereferenced by accident.

You also have a tool. lux convert go program.lux prints the Go translation of anything you have written, and reading a few of those against the originals is the fastest way to learn the mapping in terms of code you already understand. The output is deliberately literal and it declares helper functions a Go programmer would not, so read it as a translation rather than as a model.

From Python

PythonGo
x = 5x := 5
def f(n): return n * 2func f(n int) int { return n * 2 }
print(f"x is {x}")fmt.Printf("x is %d\n", x)
[1, 2, 3][]int{1, 2, 3}
{"a": 1}map[string]int{"a": 1}
{1, 2}map[int]struct{}{1: {}, 2: {}} — no set type
(1, "two")a struct, or two return values
for x in xs:for _, x := range xs {
for i in range(5):for i := range 5 {
[f(x) for x in xs if p(x)]a loop, or slices plus a helper
len(xs)len(xs)
xs.append(v)xs = append(xs, v)
d.get(k)v, ok := d[k]
Nonenil, and only for the types that have it
try / exceptan error return and if err != nil
raisereturn err, or panic for a bug
with open(p) as f:defer f.Close()
class C:type C struct plus methods
isinstance(x, C)a type switch, usually a sign to use an interface
int("42")strconv.Atoi("42"), returning a value and an error
"".join(xs)strings.Join(xs, "")
sorted(xs)slices.Sorted(...), or slices.Sort in place
pip installgo get

The adjustments that take longest: every type is fixed before the program runs, integers overflow instead of growing, a missing dictionary key gives you the zero value rather than an exception, indentation means nothing, and there is no truthiness so every condition must be a real bool.

From JavaScript

JavaScriptGo
const x = 5x := 5 — Go’s const is compile-time only
let x = 5x := 5
function f(n) { return n * 2 }func f(n int) int { return n * 2 }
n => n * 2func(n int) int { return n * 2 }
[1, 2, 3][]int{1, 2, 3}
{a: 1} as a recorda struct
{} as a lookup tablea map
xs.map(f)a loop, or a generic helper you write once
xs.lengthlen(xs)
xs.push(v)xs = append(xs, v)
null and undefinednil, and only one of it
try / catchan error return and if err != nil
throwreturn err, or panic for a bug
=====, which is the only kind there is
typeof x%T, or a type switch
JSON.parsejson.Unmarshal, in the standard library
async / awaitgoroutines and channels, with no function coloring
Promise.alla sync.WaitGroup
npm installgo get

The adjustments: numbers are not all floats and integer division truncates, == refuses to compare different types rather than converting them, there is no prototype chain, and concurrency is not a keyword on the function — any function can be called with go and there is no split between async and sync code.

Appendix C: Reading the Compiler

Go’s diagnostics are terse. There is no error code, no explanation, no --explain subcommand, and usually one line. That takes getting used to if you have been reading Rust’s, and the compensation is that the messages say what is wrong in words rather than in jargon, and there are not many distinct ones to learn.

The format is file:line:column: message. The compiler stops after ten and says too many errors, so a long list usually means one mistake near the top; fix that and rebuild before reading the rest.

Every message below was produced by building the file of that name in examples/errors.

Errors you will see in the first week

Declared and not used. An unused local variable is an error. Assign it to _ if you really want it, or delete it.

unusedvar/main.go:7:2: declared and not used: total

Imported and not used. Same rule for imports. Let your editor manage the import block with goimports.

unusedimport/main.go:5:2: "os" imported and not used

No new variables on the left side of :=. You used := twice on the same name. The second one should be =.

shortdecl/main.go:7:7: no new variables on left side of :=

Mismatched types. Go promotes nothing. Convert explicitly with float64(n).

mismatch/main.go:8:14: invalid operation: n * f (mismatched types int and float64)

Undefined. A typo, a missing import, or a name that is lower-case in another package and therefore not exported.

undefined/main.go:6:14: undefined: total

Missing return. Go requires the end of a function with results to be unreachable. An if/else if chain with no final else can fall off the end even when you know it cannot, so add the else or a final return.

missingreturn/main.go:9:1: missing return

Assignment mismatch. You took one value from a function that returns two, which almost always means you forgot the err.

assignmismatch/main.go:9:7: assignment mismatch: 1 variable but os.Open returns 2 values

Wrong arguments. Note that the compiler prints what you passed and what was wanted.

argcount/main.go:8:18: not enough arguments in call to add
	have (number)
	want (int, int)
argcount/main.go:9:24: too many arguments in call to add
	have (number, number, number)
	want (int, int)
argcount/main.go:10:21: cannot use "two" (untyped string constant) as int value in argument to add

Does not implement. The message names the missing method. That is the answer. Check the spelling, the signature, and whether the method has a pointer receiver while you passed a value.

notimpl/main.go:15:17: cannot use Square{…} (value of struct type Square) as Shape value in variable declaration: Square does not implement Shape (missing method Perimeter)

Cannot assign to a struct field in a map. Map elements are not addressable. Store a pointer, or read the value out, modify it, and put it back.

unaddressable/main.go:7:2: cannot assign to struct field m["a"].X in map

Non-declaration statement outside function body. := at package level. Use var or const.

outsidefunc/main.go:5:1: syntax error: non-declaration statement outside function body

Range over an int takes one variable. There is no index and value to unpack; the single variable is the number.

badrange/main.go:7:9: range over n (variable of type int) permits only one iteration variable

Panics

A panic is a run-time failure. The message is on the first line and everything after it is the stack, most recent call first, with the failing line at the top.

Index out of range. The message gives the index and the length.

panic: runtime error: index out of range [5] with length 3

Slice bounds out of range. Slicing may go up to the capacity, so this one reports capacity rather than length.

panic: runtime error: slice bounds out of range [:9] with capacity 3

Nil pointer dereference. Section 5.3.

panic: runtime error: invalid memory address or nil pointer dereference

Assignment to entry in nil map. You forgot the make. Section 7.3.

panic: assignment to entry in nil map

Interface conversion. A type assertion that was wrong. Use the comma-ok form so it reports rather than panics.

panic: interface conversion: interface {} is string, not int

Deadlock. Every goroutine is blocked. The runtime can only detect this when nothing at all can run, so a program with a live goroutine somewhere will hang instead of reporting.

fatal error: all goroutines are asleep - deadlock!

Vet

go vet reports mistakes that are legal Go. Run it with your tests; go test already runs some of its checks.

Format verbs that do not match their arguments. This is the check that pays for the others.

vet/printf/main.go:7:14: fmt.Printf format %d has arg name of wrong type string
vet/printf/main.go:8:14: fmt.Printf format %s reads arg #1, but call has 0 args

A copied lock. Section 5.4.

vet/copylock/main.go:13:13: show passes lock by value: goexamples/errors/vet/copylock.Registry contains sync.Mutex

A discarded cancel function. Section 11.6.

vet/lostcancel/main.go:10:7: the cancel function returned by context.WithTimeout should be called, not discarded, to avoid a context leak

A string(int) conversion, which gives a one-rune string rather than digits. Section 2.4.

conversion from int to string yields a string of one rune, not a string of digits

When the message is not enough

go doc <package>.<Name> prints the documentation for anything you are unsure about, offline and instantly, and it is faster than a search engine.

go build ./... builds everything rather than one package, which matters when the error is in a file you did not touch.

gofmt on a file with a syntax error will refuse to format it, so a file your editor stops reformatting is a file that no longer parses. That is often the fastest signal that you left out a brace.