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.
| Lux | Go |
|---|---|
let x = 5 | x := 5 — Go has no immutable local |
var x = 5 | x := 5 |
int, float, string, bool | int, float64, string, bool |
[int] | []int |
[[int]] | [][]int |
queue += 4 | queue = 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 xs | for _, x := range xs |
for i in 0..5 | for i := range 5 |
while c | for c |
if a && b | if 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
| Python | Go |
|---|---|
x = 5 | x := 5 |
def f(n): return n * 2 | func 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] |
None | nil, and only for the types that have it |
try / except | an error return and if err != nil |
raise | return 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 install | go 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
| JavaScript | Go |
|---|---|
const x = 5 | x := 5 — Go’s const is compile-time only |
let x = 5 | x := 5 |
function f(n) { return n * 2 } | func f(n int) int { return n * 2 } |
n => n * 2 | func(n int) int { return n * 2 } |
[1, 2, 3] | []int{1, 2, 3} |
{a: 1} as a record | a struct |
{} as a lookup table | a map |
xs.map(f) | a loop, or a generic helper you write once |
xs.length | len(xs) |
xs.push(v) | xs = append(xs, v) |
null and undefined | nil, and only one of it |
try / catch | an error return and if err != nil |
throw | return err, or panic for a bug |
=== | ==, which is the only kind there is |
typeof x | %T, or a type switch |
JSON.parse | json.Unmarshal, in the standard library |
async / await | goroutines and channels, with no function coloring |
Promise.all | a sync.WaitGroup |
npm install | go 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.