Index

Module 01 · From ScratchEnough Go

Enough Go to read these builds

The Map
The Map · Open board

The first build in this track, drawn as the walls it hits in order. Every wall on it is crossed with the handful of Go words this lesson teaches.

Before you start. This track builds seven real systems from an empty file, all in the Go language: a memory store like Redis, a message log like Kafka, a web server, a database, a job scheduler, a small Kubernetes (the program that keeps a fleet of other programs running) and a key-value store spread over several machines. No Go is assumed: if you know that a program is steps run top to bottom, and that a function is a named set of steps you hand inputs to and get answers back from, that is enough.

September 2007: a 45-minute build, and three people at a whiteboard

In 2007, one major Google program, written in the C++ language, took 45 minutes to build, and that was on a build system spread over many machines. Rob Pike told the story in a 2012 talk, “Go at Google”, and he was careful to call what came next a myth: “The origin myth for Go states that it was during one of those 45 minute builds that Go was conceived.”

The date is not a myth. On 21 September 2007, Robert Griesemer, Rob Pike and Ken Thompson started sketching the goals for a new language on a whiteboard at Google. They wanted a language that built in seconds, that read plainly to a programmer who had never seen the file before, and that was made for machines serving many people at once over a network. Go became a public open-source project on 10 November 2009.

That last goal is why these builds are in Go. A server is a kitchen. Orders come in from many tables at once, cooks work them, and something shared sits on the counter that everyone reads and writes. You will carry that kitchen through this whole lesson, and each piece of it turns out to be one Go word. There are five, and after them you can read any file in these builds.

What if the stove has no gas? The error that comes back with the plate

In many languages, a failure makes the program jump somewhere else to complain. Go does not. A Go function that can fail hands back two things: the answer, and a note saying whether anything went wrong. A cook hands you a plate and a slip. If the slip is blank, eat. If it says “no gas”, do not eat the plate.

Here is the first thing the Redis build does, in its first stage (redis/01-socket/main.go, lines 23 and 24):

ln, err := net.Listen("tcp", "127.0.0.1:6379")
if err != nil {

Read it aloud: “ask for an internet line ("tcp") to listen on, and call what comes back ln and err. If err is not empty, stop.” In Go, empty is spelled nil. The note is called an error, and it is an ordinary value, like a number. You look at it on the next line.

The seven builds check if err != nil 92 times (counted with a text search, 2026-09-28). Read it as “plate and slip, check the slip” and all 92 read themselves.

Who closes the fridge? defer, the note stuck by the door

A cook who opens the fridge must close it, whether the order went well, went badly, or was abandoned halfway. Put “close the fridge” at the bottom of the steps and a cook who leaves early skips it.

Go’s answer is one word. Write defer, then the cleanup step, right after you open the thing. It is a note stuck by the door: whenever you leave this function, by any way out, do this first. The Redis build’s handle function opens with it (redis/01-socket/main.go, line 38):

defer conn.Close()

The visitor’s connection now closes however handle ends: a goodbye, garbage, or a dropped line. Defer keeps the cleanup beside the mess it cleans. The builds use it for two things: closing a connection, and handing back a lock you will meet at the end of this lesson.

Twelve orders, one cook: the goroutine

Now the kitchen gets busy. The first Redis stage serves one visitor at a time, on purpose. While Alice is connected, Bob waits at the door, even if Alice is just thinking.

The fix in the Redis build’s fourth stage is one word (redis/04-eventloop/main.go, line 44):

go handle(conn) // <- the one word that changes everything

Put go in front of a function call and that function starts running alongside everything else, and the line after it runs straight away without waiting. The kitchen just hired a cook for Bob. Each of these running-alongside functions is a goroutine. The Redis build and the web server build both start one goroutine per visitor.

Here the kitchen stops matching. A cook is a person you pay. A goroutine is so cheap that the lesson’s own program starts 100,000 of them in 78 milliseconds, measured with go run ./03-goroutines on a laptop on 2026-09-28. Go runs all of them on a handful of real processor cores (the parts of the chip that actually run steps), switching between them whenever one is waiting.

The rail between the counter and the cooks: the channel

How do orders reach the cooks? A kitchen answers with a ticket rail: a metal bar with a fixed number of hooks. The waiter clips a ticket on. A free cook pulls one off. If every hook is full, the waiter stands there holding the ticket until a hook frees up. If the rail is empty, a free cook waits for a ticket.

flowchart LR
  W["waiter with 12 tickets"] -->|"clips one on, waits if all 4 hooks are full"| R["rail: 4 hooks"]
  R -->|"a free cook pulls one off"| C1["cook 1"]
  R --> C2["cook 2"]
  R --> C3["cook 3"]
  C1 -->|"takes the key, adds one, gives the key back"| T["tally sheet on the counter"]
  C2 --> T
  C3 --> T

Go has that rail built in, and it is called a channel. make(chan Ticket, 4) builds a rail with 4 hooks. rail <- t clips ticket t on, and waits if the rail is full. <-rail takes one off, and waits if it is empty. The numbers here (12 tickets, 4 hooks, 3 cooks) are teaching values, small enough to follow by eye.

The web server build uses a channel for exactly the “waits when full” half (http/06-concurrent/main.go, line 23):

var sem = make(chan struct{}, 100) // at most 100 connections in flight

A rail of 100 hooks with blank tickets (struct{} is Go’s empty value: a ticket with nothing written on it). Before starting a cook for a new visitor, the server clips one on. After the hundredth, the next visitor waits at the door until a cook finishes and pulls one off.

Two cooks, one tally sheet: the mutex

Cooks share things. In the Redis build, every goroutine reads and writes one shared table of names and values. In the kitchen, every cook adds a mark to one tally sheet on the counter.

Adding a mark is three steps: read the number, add one, write it back. Two cooks can both read 41, both write 42, and one plate goes uncounted. The lesson’s fourth program does exactly that with no protection. Four cooks each add 100,000 to one counter. Measured with go run ./04-race on 2026-09-28, it printed expected 400000, got 195493. More than half the marks vanished, and the program said nothing was wrong.

The fix is a key hanging by the sheet. A cook takes the key, marks the sheet, and hangs the key back. A second cook who wants to write waits for the key. Go calls the key a mutex, from “mutual exclusion”, and the steps are mu.Lock() and mu.Unlock(). The Redis build’s expiry stage puts it together with the word from earlier (redis/05-expiry/main.go, lines 85 and 86):

mu.Lock()
defer mu.Unlock()

Take the key, and stick a note by the door saying “hang the key back on your way out”.

Go also ships a tool that catches a forgotten key for you. Add -race to the run command and Go watches every read and write while the program runs. When two goroutines touch the same thing with no key between them, it prints both of their lines and fails.

You now know

  • A Go function that can fail returns an error value alongside its answer, and the caller checks if err != nil on the next line. Nothing jumps past the caller.
  • defer runs a cleanup step whenever the function returns, by any path. The builds use it to close connections and release locks.
  • Writing go before a function call starts a goroutine: it runs alongside the rest of the program. Goroutines are cheap enough to start one per visitor.
  • A channel passes values between goroutines. Sending waits when it is full and receiving waits when it is empty, so a channel with 100 slots works as a limit.
  • Two goroutines adding to one shared number at the same moment can lose updates, with no warning. A mutex is a lock that lets one goroutine write at a time, and running with go run -race reports any shared value left unlocked.

Stop here if you want. What follows is the rest of the vocabulary (maps, structs, slices, and how to run and test a stage), with every program run and its output pasted, for when you want to read a build line by line.

Second read: four small programs, and where each word lives in the builds

Every number below printed by a program was measured on 2026-09-28 on an Apple Silicon Mac with go1.27.1. The 12 tickets, 4 hooks and 3 cooks are teaching values; the timings and the lost-update count change on every run and on every machine.

Errors and defer

cd teaching/code/from-scratch/enough-go
go run ./01-errors
Run it yourself: full source file
  1. Install Go from go.dev/dl. Check it with go version.
  2. Make the folder: mkdir -p teaching/code/from-scratch/enough-go. Save the file below at the path shown above it, including any subfolders.
  3. Inside that folder, run go mod init lesson once. Go needs a module name to build.
  4. Run the commands in the block above from the folder where you ran mkdir.
teaching/code/from-scratch/enough-go/01-errors/main.go
package main

// 01 — errors are values, and defer.
//
//   Run:  go run ./01-errors
//
// A Go function that can fail hands back TWO things: the answer, and an error.
// The caller looks at the error before touching the answer. Nothing is thrown;
// nothing jumps past you. Every build in this track opens with the same shape:
//
//   ln, err := net.Listen("tcp", "127.0.0.1:6379")
//   if err != nil { ... }
//
// defer says "run this when the function returns, however it returns".
// The builds use it to close a connection and to release a lock.

import (
	"fmt"
	"os"
	"strconv"
)

// parsePort turns text like "6379" into a number, or says why it cannot.
func parsePort(s string) (int, error) {
	n, err := strconv.Atoi(s)
	if err != nil {
		return 0, fmt.Errorf("port %q is not a number", s)
	}
	if n < 1 || n > 65535 {
		return 0, fmt.Errorf("port %d is out of range", n)
	}
	return n, nil // nil = "no error"
}

// serve opens, maybe leaves early, and always closes.
func serve(name string) {
	fmt.Println("open  ", name)
	defer fmt.Println("close ", name) // runs on EVERY way out of serve
	if name == "bob" {
		fmt.Println("  bob sent garbage, leaving early")
		return
	}
	fmt.Println("  answered", name)
}

func main() {
	for _, s := range []string{"6379", "sixty", "99999"} {
		port, err := parsePort(s)
		if err != nil {
			fmt.Println("error:", err)
			continue
		}
		fmt.Println("port:", port)
	}

	_, err := os.ReadFile("no-such-file.txt")
	fmt.Println("error:", err)

	serve("alice")
	serve("bob")
}
port: 6379
error: port "sixty" is not a number
error: port 99999 is out of range
error: open no-such-file.txt: no such file or directory
open   alice
  answered alice
close  alice
open   bob
  bob sent garbage, leaving early
close  bob

parsePort returns (int, error): a number and a slip. It builds its own error with fmt.Errorf, and returns nil in the error slot when all went well. The fourth line is an error the standard library made, from os.ReadFile. The last six lines are defer: Bob leaves early through a return, and close bob still prints.

The three shapes data takes: map, struct, slice

A map finds a value by its name. map[string]int{"dal": 3} is a map from text to numbers. The Redis build’s whole database is one line (redis/03-store/main.go, line 25): var store = map[string]string{}. Looking a name up comes in the same two-value shape as errors: v, ok := store[key] gives the value and whether it was there at all (redis/04-eventloop/main.go, line 69). Walking a map with for k, v := range m visits the entries in a random order; the Redis expiry stage leans on that as a free random sample (redis/05-expiry/main.go, line 89).

A struct bundles named fields into one thing, and a method is a function that belongs to one, written with the struct in brackets before the name. The database build opens with both (database/01-append/main.go, lines 21 and 28): type DB struct{ f *os.File }, then func (db *DB) set(key, val string). The * means the method works on the one real DB, not a copy, so it can change it. Without the *, Go would hand the method its own duplicate of the DB, and any change it made would vanish when it returned.

A slice is a list that grows. append(list, x) returns the list with x on the end. The B-tree stage splits a node with it (database/04-btree/main.go, line 77): z.keys = append(z.keys, y.keys[t:]...). y.keys[t:] is “everything from position t on”, and ... passes each item rather than the list as one thing.

cd teaching/code/from-scratch/enough-go
go run ./02-shapes
Run it yourself: full source file
  1. Install Go from go.dev/dl. Check it with go version.
  2. Make the folder: mkdir -p teaching/code/from-scratch/enough-go. Save the file below at the path shown above it, including any subfolders.
  3. Inside that folder, run go mod init lesson once. Go needs a module name to build.
  4. Run the commands in the block above from the folder where you ran mkdir.
teaching/code/from-scratch/enough-go/02-shapes/main.go
package main

// 02 — the three shapes data takes in these builds: map, struct, slice.
//
//   Run:   go run ./02-shapes
//   Test:  go test ./02-shapes
//
// A map finds a value by its name (Redis's whole database is one).
// A struct bundles named fields; a method is a function that belongs to it
// (the database build's DB, the B-tree's node). A slice is a list that grows
// with append (the B-tree's keys, the Kafka log's segments).

import (
	"errors"
	"fmt"
)

// Ticket is one order: which table, which dish, how many.
type Ticket struct {
	Table int
	Dish  string
	Qty   int
}

// Kitchen holds what is left of each dish, and every ticket it has cooked.
type Kitchen struct {
	stock  map[string]int // dish -> portions left
	cooked []Ticket       // grows by one per ticket
}

var ErrSoldOut = errors.New("sold out")

// Cook is a method: it belongs to *Kitchen and can change it.
func (k *Kitchen) Cook(t Ticket) error {
	left, ok := k.stock[t.Dish] // ok is false if the dish is not on the menu
	if !ok {
		return fmt.Errorf("table %d: no such dish %q", t.Table, t.Dish)
	}
	if left < t.Qty {
		return fmt.Errorf("table %d: %s: %w", t.Table, t.Dish, ErrSoldOut)
	}
	k.stock[t.Dish] = left - t.Qty
	k.cooked = append(k.cooked, t)
	return nil
}

func main() {
	k := &Kitchen{stock: map[string]int{"dal": 3, "rice": 5}}

	tickets := []Ticket{
		{Table: 1, Dish: "dal", Qty: 2},
		{Table: 2, Dish: "rice", Qty: 1},
		{Table: 3, Dish: "dal", Qty: 2},
		{Table: 4, Dish: "pizza", Qty: 1},
	}
	for _, t := range tickets {
		if err := k.Cook(t); err != nil {
			fmt.Println("error:", err)
			continue
		}
		fmt.Printf("cooked table %d: %d x %s\n", t.Table, t.Qty, t.Dish)
	}
	fmt.Println("cooked", len(k.cooked), "of", len(tickets), "tickets")
	fmt.Println("dal left:", k.stock["dal"], " rice left:", k.stock["rice"])
}
cooked table 1: 2 x dal
cooked table 2: 1 x rice
error: table 3: dal: sold out
error: table 4: no such dish "pizza"
cooked 2 of 4 tickets
dal left: 1  rice left: 4

The kitchen’s stock is a map, a ticket is a struct, Cook is a method returning an error, and cooked is a slice that grows by one per plate. Table 3 fails the stock check; table 4 fails the ok check because “pizza” was never in the map.

go run and go test

go run ./02-shapes compiles the folder and runs its main function. Every stage in all seven builds is run that way, and none of the builds ships a test file yet. go test is how you check your own change to one: it runs every function named TestSomething in the folder’s _test.go files, and a test fails by calling t.Errorf.

cd teaching/code/from-scratch/enough-go
go test -v ./02-shapes
Run it yourself: all 2 source files
  1. Install Go from go.dev/dl. Check it with go version.
  2. Make the folder: mkdir -p teaching/code/from-scratch/enough-go. Save each of the 2 files below at the path shown above it, including any subfolders.
  3. Inside that folder, run go mod init lesson once. Go needs a module name to build.
  4. Run the commands in the block above from the folder where you ran mkdir.
teaching/code/from-scratch/enough-go/02-shapes/kitchen_test.go
package main

// go test ./02-shapes runs every func TestXxx in this folder.
// A test is ordinary Go that calls t.Errorf when the answer is wrong.

import (
	"errors"
	"testing"
)

func TestCookTakesFromStock(t *testing.T) {
	k := &Kitchen{stock: map[string]int{"dal": 3}}
	if err := k.Cook(Ticket{Table: 1, Dish: "dal", Qty: 2}); err != nil {
		t.Fatalf("first ticket: unexpected error %v", err)
	}
	if got := k.stock["dal"]; got != 1 {
		t.Errorf("dal left = %d, want 1", got)
	}
	if got := len(k.cooked); got != 1 {
		t.Errorf("cooked = %d tickets, want 1", got)
	}
}

func TestCookRefusesWhenSoldOut(t *testing.T) {
	k := &Kitchen{stock: map[string]int{"dal": 1}}
	err := k.Cook(Ticket{Table: 3, Dish: "dal", Qty: 2})
	if !errors.Is(err, ErrSoldOut) {
		t.Fatalf("err = %v, want ErrSoldOut", err)
	}
	if got := k.stock["dal"]; got != 1 {
		t.Errorf("a refused ticket changed the stock: dal left = %d, want 1", got)
	}
}
teaching/code/from-scratch/enough-go/02-shapes/main.go
package main

// 02 — the three shapes data takes in these builds: map, struct, slice.
//
//   Run:   go run ./02-shapes
//   Test:  go test ./02-shapes
//
// A map finds a value by its name (Redis's whole database is one).
// A struct bundles named fields; a method is a function that belongs to it
// (the database build's DB, the B-tree's node). A slice is a list that grows
// with append (the B-tree's keys, the Kafka log's segments).

import (
	"errors"
	"fmt"
)

// Ticket is one order: which table, which dish, how many.
type Ticket struct {
	Table int
	Dish  string
	Qty   int
}

// Kitchen holds what is left of each dish, and every ticket it has cooked.
type Kitchen struct {
	stock  map[string]int // dish -> portions left
	cooked []Ticket       // grows by one per ticket
}

var ErrSoldOut = errors.New("sold out")

// Cook is a method: it belongs to *Kitchen and can change it.
func (k *Kitchen) Cook(t Ticket) error {
	left, ok := k.stock[t.Dish] // ok is false if the dish is not on the menu
	if !ok {
		return fmt.Errorf("table %d: no such dish %q", t.Table, t.Dish)
	}
	if left < t.Qty {
		return fmt.Errorf("table %d: %s: %w", t.Table, t.Dish, ErrSoldOut)
	}
	k.stock[t.Dish] = left - t.Qty
	k.cooked = append(k.cooked, t)
	return nil
}

func main() {
	k := &Kitchen{stock: map[string]int{"dal": 3, "rice": 5}}

	tickets := []Ticket{
		{Table: 1, Dish: "dal", Qty: 2},
		{Table: 2, Dish: "rice", Qty: 1},
		{Table: 3, Dish: "dal", Qty: 2},
		{Table: 4, Dish: "pizza", Qty: 1},
	}
	for _, t := range tickets {
		if err := k.Cook(t); err != nil {
			fmt.Println("error:", err)
			continue
		}
		fmt.Printf("cooked table %d: %d x %s\n", t.Table, t.Qty, t.Dish)
	}
	fmt.Println("cooked", len(k.cooked), "of", len(tickets), "tickets")
	fmt.Println("dal left:", k.stock["dal"], " rice left:", k.stock["rice"])
}
=== RUN   TestCookTakesFromStock
--- PASS: TestCookTakesFromStock (0.00s)
=== RUN   TestCookRefusesWhenSoldOut
--- PASS: TestCookRefusesWhenSoldOut (0.00s)
PASS
ok  	enoughgo/02-shapes	0.083s

The second test is the useful one: a refused ticket must not touch the stock. errors.Is(err, ErrSoldOut) asks “is this, or does it wrap, the sold-out error”, which is why Cook builds its message with %w rather than %v.

Goroutines, a channel and a mutex together

cd teaching/code/from-scratch/enough-go
go run ./03-goroutines
Run it yourself: full source file
  1. Install Go from go.dev/dl. Check it with go version.
  2. Make the folder: mkdir -p teaching/code/from-scratch/enough-go. Save the file below at the path shown above it, including any subfolders.
  3. Inside that folder, run go mod init lesson once. Go needs a module name to build.
  4. Run the commands in the block above from the folder where you ran mkdir.
teaching/code/from-scratch/enough-go/03-goroutines/main.go
package main

// 03 — goroutines, a channel between them, and a mutex around shared data.
//
//   Run:  go run ./03-goroutines
//
// `go f()` starts f running alongside everything else: a goroutine.
// A channel is a rail with a fixed number of hooks: send puts a ticket on it
// (and waits if every hook is full), receive takes one off (and waits if the
// rail is empty). A mutex is a key: only the goroutine holding it may touch
// the shared tally. The Redis build runs one goroutine per client and guards
// its map with a mutex; the HTTP build uses a channel of 100 hooks as a limit.

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

type Ticket struct {
	Table int
	Dish  string
}

func main() {
	rail := make(chan Ticket, 4) // a rail with 4 hooks
	done := make(chan int)       // each cook reports here when the rail is empty

	var mu sync.Mutex
	served := map[string]int{} // shared by every cook: guard it

	for c := 1; c <= 3; c++ {
		go func() { // three cooks, running at once
			n := 0
			for t := range rail { // take tickets until the rail is closed and empty
				mu.Lock()
				served[t.Dish]++
				mu.Unlock()
				n++
			}
			done <- n
		}()
	}

	dishes := []string{"dal", "rice", "roti"}
	for i := 0; i < 12; i++ {
		rail <- Ticket{Table: i + 1, Dish: dishes[i%3]} // waits if 4 are already waiting
	}
	close(rail) // no more tickets tonight

	total := 0
	for c := 1; c <= 3; c++ {
		total += <-done
	}

	names := make([]string, 0, len(served))
	for d := range served {
		names = append(names, d) // map order is random; sort for a stable print
	}
	sort.Strings(names)
	for _, d := range names {
		fmt.Printf("%-5s %d\n", d, served[d])
	}
	fmt.Println("tickets cooked:", total)

	// Where the kitchen stops matching: a cook is a person; a goroutine is cheap.
	start := time.Now()
	wake := make(chan struct{})
	var wg sync.WaitGroup
	for i := 0; i < 100_000; i++ {
		wg.Add(1)
		go func() { defer wg.Done(); <-wake }()
	}
	fmt.Printf("100000 goroutines started in %v\n", time.Since(start).Round(time.Millisecond))
	close(wake)
	wg.Wait()
}
dal   4
rice  4
roti  4
tickets cooked: 12
100000 goroutines started in 78ms

Three cooks pull 12 tickets off a 4-hook rail. close(rail) tells the cooks no more tickets are coming, so each cook’s for t := range rail loop ends once the rail is empty, and each reports its count on a second channel, done. The last line is the kitchen’s limit: three more runs printed 83, 68 and 93 ms. The last part uses sync.WaitGroup, a counter the program waits on until every goroutine has called Done; the builds do not need it, because a server’s goroutines never all finish.

The race, caught

cd teaching/code/from-scratch/enough-go
go run ./04-race
Run it yourself: full source file
  1. Install Go from go.dev/dl. Check it with go version.
  2. Make the folder: mkdir -p teaching/code/from-scratch/enough-go. Save the file below at the path shown above it, including any subfolders.
  3. Inside that folder, run go mod init lesson once. Go needs a module name to build.
  4. Run the commands in the block above from the folder where you ran mkdir.
teaching/code/from-scratch/enough-go/04-race/main.go
package main

// 04 — the same tally with the mutex removed: a data race.
//
//   Run:   go run ./04-race
//   Race:  go run -race -trimpath ./04-race
//
// Four cooks each add 100,000 to one shared counter, with no lock. "count++"
// is three steps (read, add one, write back), and two cooks can read the
// same old value, so one of their additions is lost. -race watches every
// memory access while the program runs and reports the two goroutines that
// touched the counter with nothing ordering them.

import (
	"fmt"
	"sync"
)

func main() {
	count := 0
	var wg sync.WaitGroup
	for c := 0; c < 4; c++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for i := 0; i < 100_000; i++ {
				count++ // no mu.Lock() here: the bug
			}
		}()
	}
	wg.Wait()
	fmt.Println("expected 400000, got", count)
}
expected 400000, got 195493

204,507 additions were lost (400,000 minus 195,493, about 51%). Now the same program under the race detector. -trimpath only shortens the file paths in the report:

cd teaching/code/from-scratch/enough-go
go run -race -trimpath ./04-race
Run it yourself: full source file
  1. Install Go from go.dev/dl. Check it with go version.
  2. Make the folder: mkdir -p teaching/code/from-scratch/enough-go. Save the file below at the path shown above it, including any subfolders.
  3. Inside that folder, run go mod init lesson once. Go needs a module name to build.
  4. Run the commands in the block above from the folder where you ran mkdir.
teaching/code/from-scratch/enough-go/04-race/main.go
package main

// 04 — the same tally with the mutex removed: a data race.
//
//   Run:   go run ./04-race
//   Race:  go run -race -trimpath ./04-race
//
// Four cooks each add 100,000 to one shared counter, with no lock. "count++"
// is three steps (read, add one, write back), and two cooks can read the
// same old value, so one of their additions is lost. -race watches every
// memory access while the program runs and reports the two goroutines that
// touched the counter with nothing ordering them.

import (
	"fmt"
	"sync"
)

func main() {
	count := 0
	var wg sync.WaitGroup
	for c := 0; c < 4; c++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for i := 0; i < 100_000; i++ {
				count++ // no mu.Lock() here: the bug
			}
		}()
	}
	wg.Wait()
	fmt.Println("expected 400000, got", count)
}
==================
WARNING: DATA RACE
Read at 0x00c000092038 by goroutine 8:
  main.main.func1()
      enoughgo/04-race/main.go:27 +0x88

Previous write at 0x00c000092038 by goroutine 7:
  main.main.func1()
      enoughgo/04-race/main.go:27 +0x98

Goroutine 8 (running) created at:
  main.main()
      enoughgo/04-race/main.go:24 +0x6c

Goroutine 7 (running) created at:
  main.main()
      enoughgo/04-race/main.go:24 +0x6c
==================
==================
WARNING: DATA RACE
Write at 0x00c000092038 by goroutine 6:
  main.main.func1()
      enoughgo/04-race/main.go:27 +0x98

Previous write at 0x00c000092038 by goroutine 7:
  main.main.func1()
      enoughgo/04-race/main.go:27 +0x98

Goroutine 6 (running) created at:
  main.main()
      enoughgo/04-race/main.go:24 +0x6c

Goroutine 7 (running) created at:
  main.main()
      enoughgo/04-race/main.go:24 +0x6c
==================
expected 400000, got 342083
Found 2 data race(s)
exit status 66

Read each report from the top: what was touched (Read at), where (line 27, count++), which other goroutine touched it before with nothing in between, and where both were started (line 24, the go func()). Exit status 66 is the race detector’s default failure code, so a test run with -race fails loudly. The count came out different again (342,083) because the detector slows every access and changes the timing; it finds the race without the count needing to be wrong.

Where each word lives in the builds

One row per word: the word, the real line, and the file it is in.

WordReal lineFile
errorln, err := net.Listen("tcp", "127.0.0.1:6379")redis/01-socket/main.go, line 23
deferdefer conn.Close()redis/01-socket/main.go, line 38
mapvar store = map[string]string{}redis/03-store/main.go, line 25
struct and methodfunc (db *DB) set(key, val string)database/01-append/main.go, line 28
slicez.keys = append(z.keys, y.keys[t:]...)database/04-btree/main.go, line 77
goroutinego handle(conn)redis/04-eventloop/main.go, line 44
goroutine, in the backgroundgo activeExpiryLoop()redis/05-expiry/main.go, line 44
channelsem <- struct{}{}http/06-concurrent/main.go, line 60
mutexmu sync.Mutexredis/06-eviction/main.go, line 35
read-write mutexmu sync.RWMutexredis/04-eventloop/main.go, line 32

The last row is a mutex with two kinds of key: many goroutines may hold a reading key at once (mu.RLock()), but a writing key (mu.Lock()) waits until nobody holds either. The Redis build uses it because reads outnumber writes. Everything else the builds use (bufio, strings, os.OpenFile, sort, container/heap) is a standard-library package, and each build’s own lesson explains the one it reaches for.

Try it

In teaching/code/from-scratch/enough-go, run go vet ./... first. go vet reads the code without running it and flags mistakes a compiler allows; it prints nothing when all four programs are clean. Then five-minute experiments:

  1. In 04-race/main.go, add a sync.Mutex and wrap count++ in mu.Lock() and mu.Unlock(). Rerun both commands. The count comes out at exactly 400,000 and -race prints no report.
  2. In 03-goroutines/main.go, change the rail to make(chan Ticket) (no hooks at all). It still cooks 12 tickets: every send now waits for a cook to be ready to take it by hand.
  3. In 02-shapes/main.go, delete the if !ok check in Cook (and the , ok it reads) and rerun. Table 4 now says pizza: sold out instead of no such dish: a name missing from a map reads as 0, and 0 portions is less than 1. go test still passes, so write a test that catches it.
  4. In 01-errors/main.go, delete the word defer from the serve function’s close line. close alice and close bob now print straight after open, before any work: without defer, the line runs where it is written.

Go deeper

The full From Scratch list is in CURRICULUM.

Retrieval practice

8 questions. Answer each one before you open it.

A Go function returns (v, err). What do you do before touching v, and why does Go work this way instead of throwing?

Check if err != nil on the next line, and stop or handle it if the error is not empty. The error is an ordinary value handed back beside the answer, so nothing jumps past your code: the failure is handled exactly where it happened, in plain sight.

Why does the Redis build write defer conn.Close() at the top of handle rather than conn.Close() at the bottom?

A deferred call runs whenever the function returns, by any path. handle can return early when the visitor disconnects or sends garbage; a close written at the bottom would be skipped on those paths and the connection would leak.

What does the single word go change in go handle(conn), and what does it cost compared with hiring a cook?

It starts handle running alongside the rest of the program, and the accept loop continues at once to the next visitor. A goroutine is cheap: the lesson’s program started 100,000 of them in 78 ms on a laptop, and Go runs them all on a handful of processor cores.

make(chan Ticket, 4) builds a channel with room for 4. What happens when a fifth ticket is sent before any is taken, and how does the HTTP build use that?

The send waits until a receiver takes one off. The HTTP build makes a channel of 100 empty slots, sends one before starting each visitor’s goroutine and takes it back when the visitor is done, so the 101st visitor waits: a limit on work in flight.

Four goroutines each add 100,000 to a shared counter with no lock. Why is the total short, and what fixes it?

count++ is read, add one, write back. Two goroutines can read the same old value and both write the same new one, so an addition is lost; one run printed 195,493 instead of 400,000. A mutex around the increment (mu.Lock(), mu.Unlock()) lets only one goroutine do the three steps at a time.

What does go run -race report, and why does it matter that it does not need the answer to be wrong?

It watches every memory access while the program runs and prints each pair of goroutines that touched the same data with no lock ordering them, with both lines and where each goroutine started, then exits with status 66. A race can give the right answer on one run and the wrong one on the next, so a check that waits for a wrong answer misses it.

Name the three shapes data takes in these builds and one real use of each.

A map finds a value by name (Redis’s database is map[string]string). A struct bundles named fields, with methods attached (the database build’s DB and its set). A slice is a list that grows with append (the B-tree’s keys).

What is the difference between go run ./02-shapes and go test ./02-shapes?

go run compiles the folder and runs its main function, which is how every build stage runs. go test compiles it with the folder’s _test.go files and runs every TestSomething function, passing or failing each; it is how you check a change you made.

Review From Scratch →

Saved in this browser, and to your account when you sign in.