AI Study Notebook AI-generated
The Go Programming Language
Alan A. A. Donovan and Brian Kernighan
On this page
The Go Programming Language — Chapter-by-Chapter Outline
Author: Alan A. A. Donovan and Brian W. Kernighan First published: October 26, 2015 Edition covered: First edition (Addison-Wesley Professional Computing Series, 2015). There is only one edition; the book has not been revised in a second edition. All 13 chapters are covered.
Central thesis
Go is a statically typed, compiled language designed to make writing large-scale, concurrent, and reliable programs as natural and productive as writing small scripts. It achieves this not by layering features on existing paradigms but by making deliberate omissions — no inheritance, no default parameters, no exceptions, no generics in the first edition — while providing a small set of orthogonal, composable mechanisms: goroutines and channels for concurrency, interfaces for polymorphism, and a fast garbage collector for memory safety.
The book's argument is that Go's constraints are its strength. Every feature left out reduces the surface area a programmer must understand, and every feature included was included because it solves a real problem at scale. Understanding Go well means understanding not just what the language does but why each design decision was made and what it enables.
How do you build a language that is simultaneously fast to compile, safe to run, easy to write, and productive at the scale of millions of lines of code and hundreds of engineers?
Chapter 1 — Tutorial
Central question
What does Go look like in practice, and how quickly can a programmer start writing real programs?
Main argument
The chapter functions as an accelerated tour of Go's essential features through a sequence of complete, runnable programs of increasing sophistication. Rather than introducing syntax abstractly, each section motivates a new language feature by building something useful.
Hello, World
The simplest program establishes Go's foundational structure: every Go file begins with a package declaration, followed by imports, followed by declarations. The main package is special — it defines an executable. Compilation is immediate with go run for experimentation or go build for a binary. The canonical hello-world program also demonstrates Go's native Unicode support: the string "Hello, 世界" is valid source because Go source files are always UTF-8.
Command-line arguments: the echo program
Three progressively refined implementations of echo introduce slices (os.Args), range-based for loops, the blank identifier _, and the strings.Join function. The comparison between a naive string concatenation version and strings.Join introduces the idea that idiomatic Go prefers standard library functions over manual loops.
Finding duplicate lines: maps and file I/O
The dup program introduces maps (make(map[string]int)), the bufio.Scanner for line-by-line input, and Go's error-return convention. os.Open returns both a file handle and an error; the caller is expected to check the error immediately. Three variants of the program show how to read from standard input, from named files, or from all of os.Args[1:] uniformly.
Animated GIFs: constants, composite literals, structs
The Lissajous curve generator introduces constants (const), composite literals for slices and structs, and the math package. The program produces an animated GIF by accumulating frames and writing them to os.Stdout, illustrating how Go programs interact with binary formats without ceremony.
Fetching URLs: the net/http package
A minimal fetch program calls http.Get, reads the response body with ioutil.ReadAll, and prints it. The program is fewer than fifteen lines yet performs a full HTTP request-response cycle, demonstrating how Go's standard library reduces networking to high-level calls.
Fetching URLs concurrently: goroutines and channels
The fetchall program adds a go statement before each HTTP call, launching each fetch as a goroutine. A channel (ch := make(chan string)) collects results. The program runs all fetches in parallel and finishes in the time of the slowest single fetch rather than their sum — the first concrete demonstration of Go's concurrency model.
A web server: http.HandleFunc and handlers
Three versions of a minimal HTTP server show progressively more sophisticated behavior: a simple echo server, a counter server using sync.Mutex to protect shared state across concurrent requests, and a server that inspects HTTP headers and form values. The progression introduces http.HandleFunc, goroutine-per-request handling, and the need for synchronization when goroutines share mutable state.
Loose ends
A brief survey of features not yet introduced — switches, named types, pointers, method declarations — closes the tutorial and maps forward to subsequent chapters.
Key ideas
- Programs are organized into packages;
package mainis the entry point. - Go uses
:=for short variable declaration with type inference;varfor package-level or explicit declarations. - The
forloop is the only loop construct; it covers traditional, while-style, and range-based iteration. - Slices are the go-to sequence type; arrays are fixed and rarely used directly.
- Maps are built-in hash tables with constant-time access.
- Error handling is explicit: functions return error values, callers check them.
- Goroutines are launched with
go; channels carry typed values between goroutines. - The standard library is comprehensive — HTTP, I/O, image encoding, and math are all one import away.
Key takeaway
Go's design lets a programmer write real, concurrent, networked programs in under a hundred lines, not by hiding complexity but by providing clean, composable primitives.
Chapter 2 — Program Structure
Central question
What are the rules governing names, declarations, variables, types, assignments, and scope in Go, and how do they work together?
Main argument
This chapter is a careful survey of Go's structural elements — the foundation that all subsequent chapters build on. Rather than cataloging syntax, it explains the reasoning behind each rule and the pitfalls that arise when rules are misunderstood.
Names and visibility
Go is case-sensitive and reserves 25 keywords (if, switch, func, etc.) plus roughly three dozen predeclared identifiers. The single most important naming rule: if an identifier begins with an upper-case letter, it is exported — visible outside its package. Everything else is package-private. This binary visibility model replaces the multi-level access modifiers (public, protected, private) of other languages with a single, easy-to-remember rule.
Declarations and zero values
The four declaration keywords — var, const, type, func — appear at package level or inside functions. Every variable has a well-defined zero value (0 for numbers, false for booleans, "" for strings, nil for pointers and reference types), which eliminates the class of bugs caused by uninitialized variables.
Short variable declarations and their subtlety
Inside functions, := is the most common declaration form. Its subtlety: in a block where at least one variable on the left is new, := silently reassigns existing variables rather than creating new ones. This is a frequent source of bugs when, for example, err := someCall() is used after err was declared with var err error in the same block.
Pointers
Go has pointers but no pointer arithmetic. &x yields a pointer to x; *p dereferences it. Unlike C, the only operations on pointers are dereferencing and comparison. The new(T) function allocates an unnamed variable of type T and returns its address — equivalent to var x T; return &x. The garbage collector handles deallocation.
Type declarations
type Celsius float64 creates a new named type. Even though Celsius and float64 have the same underlying representation, Go will not implicitly convert between them. This prevents accidentally mixing, say, Celsius and Fahrenheit temperatures. Explicit conversion Celsius(x) is always required, making units-of-measure errors a compile-time error rather than a runtime one.
Tuple assignment and assignability
Tuple assignment (x, y = y, x) evaluates all right-hand side expressions before any left-hand side update, making swap idioms and multi-value function returns safe. Assignability rules specify when values of different types can be assigned; interface satisfaction and nil are the main special cases.
Packages, initialization, and init()
Packages group related declarations and provide namespace isolation. Package-level variables initialize in dependency order before main runs. The init() function runs automatically at startup; a package may define multiple init functions, each called once.
Scope vs. lifetime
Scope is a compile-time concept: the region of source text in which a name refers to its declaration. Lifetime is a runtime concept: how long the variable exists in memory. Inner declarations shadow outer ones. Control structures (if, for, switch) create implicit additional lexical blocks beyond their body blocks, which affects where declarations in initialization clauses are visible.
Key ideas
- Upper-case initial letter = exported; lower-case = package-private.
- Every variable has a meaningful zero value; uninitialized does not mean undefined.
:=declares and assigns; at least one new variable must appear on the left.- Named types prevent accidental mixing of compatible-but-semantically-different values.
- Tuple assignment is atomic: all right-hand sides are evaluated before any assignment.
- Scope and lifetime are distinct concepts; confusing them is a common source of bugs.
- Package-level initialization runs in dependency order before
main.
Key takeaway
Go's structural rules are designed to eliminate entire categories of bugs — uninitialized variables, accidental type confusion, scope-related shadowing — at the cost of a small amount of explicitness.
Chapter 3 — Basic Data Types
Central question
What are Go's numeric, boolean, string, and constant types, and what are their key behaviors and idioms?
Main argument
Go's basic types cover the expected ground — integers, floats, complex numbers, booleans, strings — but several design choices differ from C or Java and have practical consequences for correctness.
Integers
Go provides eight integer types: signed int8/16/32/64 and unsigned uint8/16/32/64, plus the platform-dependent int and uint (32 or 64 bits), uintptr (pointer-sized), rune (alias for int32, used for Unicode code points), and byte (alias for uint8, used for raw data). All arithmetic operators require operands of identical types — there is no implicit numeric promotion. The % operator's sign follows the dividend (-5 % 3 == -2). Overflow silently discards high-order bits; the language provides no overflow detection.
Floating-point numbers
float32 and float64 follow IEEE 754. float64 is preferred for all numerical work because it provides roughly 15 significant digits versus 6 for float32. Special values +Inf, -Inf, and NaN can be tested with math.IsInf and math.IsNaN; comparisons with NaN always yield false, including NaN == NaN.
Complex numbers
complex64 and complex128 hold complex numbers with float32 and float64 components respectively. The complex(r, i) built-in constructs them; real() and imag() extract components. The math/cmplx package provides operations including complex square root, used in the chapter's Mandelbrot set example.
Booleans
Only true and false exist; there is no implicit conversion from integers to booleans. && and || short-circuit: the right operand is not evaluated if the left determines the result.
Strings
Strings are immutable sequences of bytes, conventionally (but not necessarily) UTF-8. len(s) returns byte count, not rune count. Individual bytes are accessed by index (s[i]); substrings by range (s[i:j]). Because strings are immutable, operations like concatenation create new strings — for efficient construction, bytes.Buffer or strings.Builder is used instead.
The range loop over a string automatically decodes UTF-8 and yields (index, rune) pairs. Indexing (s[i]) yields raw bytes; to count Unicode characters, range is needed. The unicode/utf8 package provides explicit encode/decode operations.
Constants and iota
Constants are compile-time expressions. The const block with iota — a counter that resets to 0 at each const block and increments by one per constant — is Go's enumeration mechanism. Expressions like 1 << iota create bit-flag constants. Untyped constants retain precision beyond machine arithmetic (at least 256 bits) and convert implicitly to the target type when assigned, enabling natural arithmetic like const e = 2.71828 to work with both float32 and float64 without explicit conversion.
Key ideas
- No implicit numeric type conversion; all mixed-type arithmetic requires explicit casts.
runeisint32and represents a Unicode code point;byteisuint8.- Strings are immutable byte sequences;
lenmeasures bytes, not characters. rangeover a string decodes UTF-8 automatically.NaN != NaNin IEEE 754 arithmetic; usemath.IsNaNfor detection.iotaenables clean enumeration and bit-flag constants inconstblocks.- Untyped constants adjust precision to context, avoiding boilerplate casts.
Key takeaway
Go's basic types eliminate implicit conversions and undefined behavior at the cost of requiring explicit casts, making numeric code more verbose but far less error-prone.
Chapter 4 — Composite Types
Central question
How do Go's aggregate types — arrays, slices, maps, and structs — work, and what idioms govern their use?
Main argument
Composite types are where Go diverges most visibly from C and Java. Slices (not arrays) and maps are the everyday workhorses; structs and embedding replace class hierarchies.
Arrays
Arrays have fixed length that is part of the type: [3]int and [4]int are different, incompatible types. Arrays are passed by value (copied) when passed to functions — a key difference from C. The ellipsis literal [...]int{1, 2, 3} lets the compiler count elements. Arrays are rarely used directly in Go; slices almost always serve better.
Slices
A slice is a three-component descriptor: a pointer to an underlying array, a length, and a capacity. Multiple slices can share the same underlying array, so modifying one can affect others. Key operations:
s[i:j]creates a new slice header sharing the same backing array (no copy).append(s, elem)adds to the end; if capacity is exceeded, a new, larger array is allocated (typically doubling), and the slice header is updated. The caller must capture the return value.copy(dst, src)copies between slices.make([]T, len, cap)allocates a new slice with the given length and capacity.
Slices are not comparable with == (only with nil). Idiomatic "in-place" techniques filter or modify slices by re-using the underlying array, avoiding allocations.
Maps
Maps (map[K]V) are hash tables with unordered, non-deterministic iteration (intentionally randomized in Go since Go 1.0 to prevent programs from relying on implementation-specific ordering). Key types must be comparable. Common idioms:
v, ok := m[key]safely tests presence without defaulting to zero.delete(m, key)removes entries.- Maps cannot be compared with
==and cannot have their elements addressed. - Maps of slice values (
map[string][]string) accumulate multiple values per key.
Structs
Structs are heterogeneous aggregates of named fields. Field order matters for type identity. Exported fields must start with upper-case letters. Structs are comparable if all fields are comparable, enabling their use as map keys.
Struct embedding and anonymous fields
A struct can embed another type by naming only the type, not the field: type ColoredPoint struct { Point; Color }. The fields and methods of Point are promoted to ColoredPoint, accessible without qualification. This is Go's mechanism for composition over inheritance — a ColoredPoint "has a" Point, not "is a" Point. Methods of embedded types are promoted to the outer type; the outer type can override them.
JSON marshaling and struct tags
json.Marshal converts Go structs to JSON; json.Unmarshal reverses the process. Struct field tags — raw string literals following field declarations like `json:"name,omitempty"` — control the JSON field name and whether zero values are omitted. Only exported fields are marshaled. The json.MarshalIndent function produces human-readable output with configurable indentation.
Text and HTML templates
The text/template and html/template packages provide template-driven text generation. Templates use {{.FieldName}}, {{range}}, {{if}}, and pipe (|) to transform data. html/template automatically escapes strings to prevent cross-site scripting, making it the correct choice for web output.
Key ideas
- Arrays are value types with fixed size; slices are the dynamic counterpart.
appendmay relocate the backing array; always capture its return value.- Map iteration order is deliberately randomized.
- Struct embedding promotes fields and methods, enabling composition.
- Struct field tags wire Go types to external formats like JSON without a separate schema.
html/templateescapes output automatically; use it for any web-facing text generation.
Key takeaway
Go's composite types replace the complex hierarchy of collection classes found in OO languages with four orthogonal types — arrays, slices, maps, structs — each with a small, learnable API.
Chapter 5 — Functions
Central question
What makes Go functions first-class values, and how do multiple return values, closures, deferred calls, and panic/recover change the way programs are structured?
Main argument
Functions in Go carry more semantic weight than in most languages because they are the unit of error reporting, the mechanism for resource cleanup, and the building block of concurrency. The chapter's through-line is that well-designed Go functions make failure as visible and explicit as success.
Multiple return values and the error convention
Go functions may return multiple values. The canonical idiom is (result, error): the caller receives both the result and an error simultaneously and must decide what to do with both before proceeding. This forces error handling to be local and explicit. Named return variables allow a function to document its outputs and enable "bare returns," though bare returns reduce readability in long functions.
Error handling philosophy
Unlike exceptions — which unwind the stack and can be caught anywhere — Go errors are just values returned through normal control flow. The authors describe four strategies:
- Propagate: wrap with context using
fmt.Errorfand return to caller. - Retry: for transient failures, retry with exponential backoff.
- Log and continue: for non-critical errors in long-running programs.
- Fail fast: acceptable in
mainand top-level initialization.
The rhythm of Go code is: "failure is usually dealt with before success," keeping the happy path at the outermost indentation level.
Function values and behavioral parameterization
Functions are first-class: assignable to variables, passed as arguments, stored in data structures. This enables higher-order functions like forEachNode(n *html.Node, pre, post func(*html.Node)), which walks an HTML tree and calls caller-provided functions at each node — a clean separation of traversal from action.
Anonymous functions and closures
Function literals close over variables in their enclosing scope. The squares() example returns a function that remembers and increments a counter across calls:
func squares() func() int {
var x int
return func() int { x++; return x * x }
}
A critical gotcha: loop variables are shared by all closures created in the loop. Without an explicit dir := dir shadowing assignment inside the loop body, all closures will reference the same, final loop variable value.
Variadic functions
func sum(vals ...int) accepts any number of int arguments; vals is a []int inside the function. A slice can be unpacked with sum(values...). fmt.Println and append are variadic.
Deferred function calls
defer stmt schedules stmt to execute when the enclosing function returns, whether normally or via panic, in LIFO order. Primary uses:
- Resource cleanup: pair
f, err := os.Open(...)withdefer f.Close(). - Tracing:
defer un(trace("funcName"))prints entry and exit. - Modifying named return values: deferred functions can read and change named results after the
returnstatement.
Important: deferring inside a loop runs all deferred calls only when the function exits, potentially exhausting file descriptors. Move the loop body to a helper function to scope each defer properly.
Panic and recover
panic(v) aborts normal execution, unwinds the stack running all deferred functions, and terminates the program with a stack trace. recover() inside a deferred function stops the panic and returns the panic value. The authors caution that "recovering indiscriminately from panics is a dubious practice" because variable state becomes undefined. The idiomatic safe pattern uses a sentinel type to distinguish expected panics from unexpected ones, re-panicking on the latter.
Key ideas
(result, error)return pairs force error handling at the call site.fmt.Errorfadds context to errors without losing the original; errors form chains.- Function values enable behavioral parameterization without interfaces.
- Closures capture variables by reference; loop closures share the loop variable.
deferguarantees cleanup even on early returns or panics.panicis for truly unexpected states; recoverable errors use return values.- Go's variable-size stacks (starting at 2 KB, growing to ~1 GB) make recursion safe.
Key takeaway
Go functions are designed to make control flow for success and failure equally explicit; deferred calls and panic/recover handle the exceptional cases where normal returns are insufficient.
Chapter 6 — Methods
Central question
How does Go attach behavior to types, and what is the relationship between methods, pointer receivers, and encapsulation?
Main argument
Go has no classes, but it does have methods. Any named type can have methods declared on it, not just structs. This makes methods orthogonal to data types: you can add methods to an integer type, a slice type, or a function type. The chapter argues that this generality, combined with explicit receiver parameters, makes the relationship between data and behavior clearer than traditional class-based OOP.
Method declarations
A method is a function with a receiver parameter before the function name: func (p Point) Distance(q Point) float64. The receiver is just a parameter — there is no implicit this. The same method name can exist on different types without conflict; each type has its own namespace. Methods are syntactic sugar for functions: p.Distance(q) is equivalent to Point.Distance(p, q).
Pointer receivers
When a method must modify the receiver or when the receiver is large and copying is wasteful, a pointer receiver is used: func (p *Point) ScaleBy(factor float64). Go automatically takes the address or dereferences as needed:
p.ScaleBy(2)whenpis of typePointcompiles because the compiler implicitly takes(&p).ScaleBy(2).pptr.Distance(q)whenpptris*Pointcompiles because the compiler implicitly dereferences.
Convention: if any method on a type has a pointer receiver, all methods should, for consistency.
Nil as a valid receiver
Pointer receivers can operate on nil receivers — a method on *IntList can safely handle nil as the empty list. This is used to simplify code that might otherwise require special-case nil checks before every method call.
Struct embedding and method promotion
When a struct embeds another type, the embedded type's fields and methods are promoted to the outer struct, accessible directly. The compiler generates forwarding wrappers. This is composition, not inheritance: a ColoredPoint has a Point but is not a Point. It does not satisfy interfaces that Point satisfies (unless it also implements those methods). The distinction matters for interface satisfaction.
Method values and method expressions
A method value binds a receiver to a method, creating a closure: distanceFromP := p.Distance is a func(Point) float64 with p already bound. A method expression takes the receiver as an explicit first argument: Point.Distance has type func(Point, Point) float64. Both forms are useful when APIs expect function values.
Encapsulation
Go's encapsulation unit is the package, not the type. Any code within the same package can access unexported fields. Exported (capitalized) names are the package's API. Three benefits: (1) callers need only understand the exported surface; (2) the implementation can change without breaking clients; (3) internal invariants cannot be violated from outside the package.
Key ideas
- Any named type (not just structs) can have methods.
- Receivers are explicit parameters;
p.Distance(q)isPoint.Distance(p, q). - Pointer receivers allow mutation and avoid copying; value receivers are copies.
- Nil is a valid pointer receiver in methods designed to handle it.
- Embedding promotes fields and methods; it is composition, not inheritance.
- Encapsulation is package-level, not type-level.
Key takeaway
Go methods make the association between data and behavior explicit through receiver parameters, enabling a consistent model that works across all named types without requiring class hierarchies.
Chapter 7 — Interfaces
Central question
How do Go's interfaces enable polymorphism without inheritance, and what rules govern interface satisfaction, interface values, and dynamic dispatch?
Main argument
Interfaces are Go's only mechanism for polymorphism, and they are intentionally different from Java or C# interfaces in one key respect: satisfaction is implicit. A type satisfies an interface simply by implementing all its methods — no implements declaration is needed. This means interfaces can be defined after the fact, and the standard library's interfaces can be satisfied by types it has never seen.
Interface types and implicit satisfaction
An interface type names a set of methods. A concrete type satisfies it if it possesses all those methods. The io.Writer interface requires only Write([]byte) (int, error) — any type with that method satisfies it automatically. This enables the standard library to operate on types from user packages without any coupling.
Interface values: dynamic type and dynamic value
An interface value is a pair: a dynamic type (the concrete type stored) and a dynamic value (the concrete value). An interface value is nil only when both components are nil. The critical subtlety: a non-nil interface holding a nil pointer is non-nil — calling a method on it reaches the dynamic type's method, which receives a nil pointer. This distinction causes "nil interface but non-nil concrete type" panics that confuse beginners.
Key standard interfaces
The chapter works through the library's most important interfaces:
io.Writer: write bytes to any destination — files, HTTP responses, bytes buffers, the discard sink.fmt.Stringer:String() stringenables custom%vformatting.sort.Interface:Len(),Less(i, j int),Swap(i, j)sorts any sequence. The chapter builds a multi-key sort by implementingsort.Interfaceover a[]Moviewith user-selectable comparison columns.http.Handler:ServeHTTP(ResponseWriter, *Request)makes any value a web server.error:Error() string— the simplest possible interface, yet the basis of all Go error handling.
Type assertions
x.(T) extracts the concrete value from an interface. If T is a concrete type, it checks the dynamic type and returns the value. If T is an interface, it checks whether the dynamic type satisfies T. The safe two-value form v, ok := x.(T) returns a boolean instead of panicking on mismatch.
Type switches
A switch x := x.(type) { case string: ... case int: ... } discriminates among multiple types. Within each case, x has the specific type, enabling type-specific code without explicit assertions.
The error interface and custom errors
User-defined error types implement the error interface. The os.PathError type carries both the operation name and the underlying OS error, enabling callers to distinguish "not found" from "permission denied" with a type assertion. The errors.New and fmt.Errorf functions create simple error values.
Design principles
- Interfaces should be small: the canonical Go interface has one or two methods.
- Define interfaces at the point of use, not at the point of implementation — "accept interfaces, return concrete types."
- Avoid creating interfaces just to have one; let concrete types emerge first.
- Use interfaces to decouple packages; do not use them to wrap single concrete types.
Key ideas
- Interface satisfaction is implicit; no declaration is required.
- An interface value is a (dynamic type, dynamic value) pair; nil only when both are nil.
- A non-nil interface holding a nil pointer is non-nil and can cause surprising panics.
- Type assertions extract concrete values; the two-value form is safe.
- Type switches dispatch on the runtime type of an interface value.
io.Writer,sort.Interface,http.Handler, anderrorare the library's load-bearing interfaces.
Key takeaway
Go interfaces enable duck-typed polymorphism with compile-time safety; because satisfaction is implicit, interfaces can be defined independently of implementations, decoupling packages without requiring shared base classes.
Chapter 8 — Goroutines and Channels
Central question
How does Go's Communicating Sequential Processes model turn concurrent programming into a manageable, compositional discipline?
Main argument
Go's concurrency is based on Communicating Sequential Processes (CSP), a formal model developed by Tony Hoare in 1978. The central idea: instead of sharing memory and protecting it with locks, independent activities communicate by passing values through channels. The chapter argues that this model makes concurrent programs easier to reason about because shared state is confined to single goroutines and all coordination is explicit in the channel operations.
Goroutines
A goroutine is a lightweight concurrent unit of execution, cheaper than an OS thread by orders of magnitude. go f() starts f as a new goroutine without blocking. When main returns, all goroutines are abruptly terminated — there is no background thread that keeps the process alive. The runtime schedules goroutines onto OS threads automatically, multiplexing many goroutines onto far fewer threads.
Channels
A channel is a typed conduit: ch := make(chan int). Three operations:
- Send:
ch <- x— blocks until another goroutine receives. - Receive:
x := <-ch— blocks until another goroutine sends. - Close:
close(ch)— signals no more values will be sent; subsequent receives return zero values.
Unbuffered vs. buffered channels
Unbuffered channels (default) synchronize sender and receiver: every send blocks until a matching receive occurs. They provide the strongest synchronization guarantee. Buffered channels (make(chan int, 100)) have a queue; sends block only when full, receives block only when empty. They decouple sender from receiver and can absorb bursts.
Pipelines
Channels connect goroutines into processing pipelines. One goroutine generates values into a channel; a second reads from that channel, processes, and writes to another; and so on. The range construct iterates over a channel until it is closed:
for x := range naturals {
squares <- x * x
}
Unidirectional channel types
Functions can declare channel parameters as send-only (chan<-) or receive-only (<-chan), restricting misuse and documenting intent. Bidirectional channels convert to unidirectional at the call site.
The select statement
select waits for the first ready channel operation, chosen randomly when multiple are ready:
select {
case x := <-ch1:
case ch2 <- y:
default:
// non-blocking fallback
}
select with a default branch enables non-blocking channel operations. select with a time.After channel implements timeouts.
Practical concurrency patterns
- Counting semaphores: a buffered channel limits concurrency. Sending acquires a slot; receiving releases it. Used to cap the number of simultaneous HTTP requests or open files.
- sync.WaitGroup:
Add(1)before each goroutine;defer wg.Done()inside;wg.Wait()blocks until the counter reaches zero. The standard way to wait for a batch of goroutines. - Cancellation via closed channels: closing a
donechannel broadcasts cancellation to all goroutines polling it with a non-blocking receive in aselect.
Extended examples
- Concurrent web crawler: uses a worklist channel and a seen map to perform breadth-first web crawling, with a semaphore to limit parallelism and prevent resource exhaustion.
- Concurrent directory traversal: goroutines per subdirectory with
WaitGroup; a closer goroutine closes the output channel after all workers finish. - Chat server: a broadcaster goroutine maintains a client set and dispatches messages using
select, confining the map to a single goroutine to avoid races.
Key ideas
- Goroutines are cheap: typical programs launch thousands without difficulty.
- Channels are typed; close signals completion; range over a channel reads until close.
- Unbuffered channels synchronize; buffered channels decouple.
selectmultiplexes channel operations; random choice prevents starvation.- Counting semaphores (buffered channels) are the standard way to limit parallelism.
- Closing a channel broadcasts to all receivers simultaneously — the idiom for cancellation.
- Goroutine leaks occur when goroutines block permanently; always ensure termination paths.
Key takeaway
Go's goroutine-and-channel model turns concurrency from a correctness problem (shared mutable state) into a communication problem (who sends what to whom), making the interaction between concurrent components explicit and verifiable.
Chapter 9 — Concurrency with Shared Variables
Central question
When channel-based communication is impractical, how do you safely share variables between goroutines?
Main argument
The previous chapter's CSP model works best when data flows through channels. But some problems — shared caches, reference-counted objects, monotonically increasing counters — are more naturally expressed as shared variables. This chapter covers the tools and discipline for sharing safely, and explains why shared-memory concurrency is difficult to reason about even with locks.
Race conditions
A data race occurs when two goroutines access the same variable concurrently and at least one access is a write. The result is undefined behavior: the program may produce wrong results, crash, or appear to work most of the time and fail rarely. Race conditions are pernicious because they may be latent for long periods and triggered only by specific scheduling interleavings.
The bank deposit example makes this concrete: balance = balance + amount comprises a read and a write. If two goroutines execute this simultaneously, one update can be lost — Bob's deposit disappears because Alice's goroutine read the balance before Bob's write was visible.
Three strategies to avoid data races
- Avoid writing: initialize shared data before goroutines start, then treat it as read-only.
- Confine to one goroutine: only one goroutine modifies the variable; others communicate via channels. This is the CSP principle applied to shared state.
- Mutual exclusion: multiple goroutines can access but only one at a time, enforced by a lock.
sync.Mutex
mu.Lock() acquires exclusive access; mu.Unlock() releases. Code between them is the critical section. Canonical pattern: defer mu.Unlock() immediately after a successful lock, ensuring release even on panic or early return. Go mutexes are not reentrant: a goroutine that already holds a lock will deadlock if it tries to lock again. This prevents a class of bugs but requires factoring internal helper functions that operate on already-locked state.
sync.RWMutex
A read/write mutex allows multiple concurrent readers (RLock/RUnlock) or a single exclusive writer (Lock/Unlock). Suitable when reads dominate. The extra bookkeeping makes it slower than sync.Mutex for uncontended access.
Memory synchronization and the memory model
Processors cache memory and reorder writes. Without synchronization, two goroutines may observe different orderings of each other's writes. sync.Mutex and channel operations act as synchronization barriers: they force the processor to flush buffered writes to main memory. The Go memory model specifies which operations are guaranteed to be visible to other goroutines.
sync.Once for lazy initialization
once.Do(init) ensures init is called exactly once, with all goroutines blocked until it completes and a memory barrier guaranteeing they see the initialized state. This is the correct pattern for lazily initializing expensive objects (database connections, large tables) in a concurrent program.
The race detector
Compiling with -race instruments the binary to track all variable accesses and synchronization operations at runtime. When a data race is detected, it prints the two conflicting accesses with stack traces. The race detector adds significant overhead but is invaluable during testing. It cannot prove the absence of races — it only reports races that actually occur during a run.
Goroutines vs. OS threads
- Stack size: OS threads have fixed 2 MB stacks; goroutines start at ~2 KB and grow on demand up to ~1 GB. This allows millions of goroutines where millions of threads would be impossible.
- Scheduling: the Go runtime uses m:n scheduling, multiplexing
mgoroutines ontonOS threads. Context switches between goroutines are much cheaper than OS-level thread switches. - GOMAXPROCS: controls how many OS threads run Go code simultaneously. Defaults to the number of CPUs. Goroutines blocked on I/O do not consume a GOMAXPROCS slot.
- No identity: goroutines have no programmer-accessible ID, preventing thread-local storage patterns that lead to action-at-a-distance bugs.
Key ideas
- A data race is any concurrent read+write to the same variable without synchronization.
defer mu.Unlock()is the idiomatic lock-release pattern.- Go mutexes are non-reentrant; internal helpers must receive pre-locked state.
- Channel operations and mutex operations are memory synchronization barriers.
sync.Onceis the correct lazy initialization primitive.-raceflag enables the race detector; use it during testing.- Goroutines are ~1000× cheaper than OS threads due to small stacks and m:n scheduling.
Key takeaway
Shared-memory concurrency is safe in Go when disciplined — three strategies (immutability, confinement, mutual exclusion) cover all cases, and the race detector catches violations during development.
Chapter 10 — Packages and the Go Tool
Central question
How does Go's package system enable modular, reusable code at scale, and how does the go tool support the full development lifecycle?
Main argument
Go's package system is designed for very large codebases. Its key properties — explicit imports, acyclic dependencies, and globally unique import paths — make compilation fast, dependency graphs tractable, and API boundaries clear. The go tool integrates package management, building, testing, and documentation into a single command, eliminating the configuration sprawl of traditional build systems.
Package fundamentals
Every Go source file begins with a package declaration. Packages are the unit of encapsulation; unexported names are invisible outside the package. Each package has an import path — a globally unique string like "github.com/user/project/util" — and a package name (conventionally the last path segment, e.g., util). The import path is how the compiler finds source code; the package name is how the code is referenced.
Naming conventions
Package names should be short, lowercase, and singular. The package name and its exported names form compound identifiers: http.Get, rand.Intn, os.Open. When two packages have the same last segment, renaming resolves conflicts: import mrand "math/rand".
Blank imports and init()
import _ "image/png" imports a package solely for its side effects — specifically, the init() function that registers the PNG decoder with the image package. This mechanism allows deferred registration: the image package ships without PNG support built in; importing image/png adds it. The binary only includes the codecs the program actually imports, keeping executables small.
The go tool commands
| Command | Purpose |
|---|---|
go build |
Compile; discard output (create binary for main) |
go install |
Compile and cache in $GOPATH/pkg |
go get |
Download and install packages from remote |
go run |
Compile and immediately execute |
go test |
Compile and run tests |
go doc |
Display documentation |
go list |
Query package metadata |
go fmt |
Format source code |
go vet |
Run static analysis checks |
Workspace and GOPATH
The $GOPATH workspace has three directories: src (source, organized by import path), pkg (compiled packages, organized by platform), and bin (installed executables). The directory structure mirrors import paths: gopl.io/ch1/helloworld lives at $GOPATH/src/gopl.io/ch1/helloworld.
Fast compilation
Go compiles faster than most compiled languages for three reasons: (1) imports are explicit — the compiler does not scan directories; (2) the dependency graph is acyclic — packages compile in parallel; (3) compiled packages record their export information for efficient re-use.
Internal packages
Packages with internal in their import path (net/http/internal/chunked) can only be imported by packages within the parent tree. This allows large projects to share implementation helpers without exposing them publicly.
Documentation
Documentation comments immediately precede declarations: // Fprintf formats... becomes the documentation for fmt.Fprintf. go doc fmt.Fprintf displays it at the command line; godoc renders the entire package as a web page with cross-links. The go doc convention eliminates the need for a separate documentation system.
Key ideas
- Import paths are globally unique; package names are local aliases.
- Blank imports trigger
init()side effects without using exported names. - Acyclic package dependencies enable parallel compilation.
go getuses the import path as the source URL; no package manager configuration needed.internalpackages restrict import to the parent tree.- Documentation is source comments;
go docandgodocrender them.
Key takeaway
Go's package system is an opinionated choice to make the common case — one developer, one tool, explicit dependencies — fast and frictionless, at the cost of flexibility for unusual build configurations.
Chapter 11 — Testing
Central question
What is Go's approach to testing, and how do the go test tool and testing conventions support reliable software development?
Main argument
Go takes a deliberately low-tech approach to testing: one command (go test), a small set of conventions, and a minimal standard library. There is no assertion framework, no test runner configuration file, no dependency injection container. The authors argue that this minimalism forces programmers to write clear, explicit test code that serves as documentation, rather than hiding test logic behind opaque abstractions.
The go test tool
go test scans files ending in _test.go (excluded from non-test builds) and identifies three function categories by naming convention:
- Test functions (
func TestXxx(t *testing.T)): callt.Error/t.Errorffor soft failures,t.Fatal/t.Fatalfto abort. - Benchmark functions (
func BenchmarkXxx(b *testing.B)): execute in a loop controlled byb.N;go test -bench=.runs them. - Example functions (
func ExampleXxx()): provide executable documentation; the output comment is matched against actual output.
Table-driven testing
The dominant Go test pattern: organize test cases as a slice of structs with inputs and expected outputs, loop over them, and report all failures rather than stopping at the first. This makes adding new cases trivial and prevents "hiding" multiple failures behind a single early exit.
Randomized testing
For properties that hold for all inputs, generate random inputs and either compare against a simpler reference implementation or verify invariants. Log the random seed so failures are reproducible: t.Logf("random seed: %d", seed).
Black-box vs. white-box testing
- Black-box tests use only the package's exported API; they test what a client sees and are robust to implementation changes.
- White-box tests access unexported functions and state; they enable precise testing of complex internal logic but are fragile when internals change.
External test packages
Test files can declare package foo_test instead of package foo, creating an external test package. This resolves import cycles (a test package for net/http can import packages that depend on net/http) and enforces that tests use only the public API. The export_test.go convention exposes internal symbols specifically for external tests without including them in the production build.
Test quality
Effective tests produce failure messages of the form "f(x) = y, want z" — they report the operation, the actual result, and the expected result. They do not use generic assertion helpers that hide context. Tests should report multiple failures per run rather than halting at the first. Tests warrant the same quality of code as production.
Coverage
go test -coverprofile=c.out instruments the binary to record which statements are executed during tests. go tool cover -html=c.out renders a color-coded HTML view. Coverage is a useful starting point but not a goal: 100% coverage does not imply correctness.
Benchmarking and profiling
Benchmark functions run their loop body b.N times; the framework adjusts N until results are statistically stable. go test -cpuprofile, -memprofile, and -blockprofile generate pprof-compatible profiles. go tool pprof visualizes them to identify hotspots.
Key ideas
- Test, benchmark, and example functions are identified by naming convention, not annotations.
- Table-driven tests minimize boilerplate and maximize case coverage.
- Log random seeds for reproducible randomized tests.
- External test packages (
package foo_test) avoid import cycles and enforce API boundaries. - Failure messages should be self-contained: operation + actual + expected.
- Coverage tools show which code is exercised; coverage alone does not prove correctness.
- Benchmark + pprof identifies real performance bottlenecks with evidence.
Key takeaway
Go's testing philosophy is that explicit, readable test code is more valuable than framework magic; the go test tool's minimal conventions make testing a natural part of the development workflow.
Chapter 12 — Reflection
Central question
When must a program inspect and manipulate values whose types are not known at compile time, and how does Go's reflect package support this?
Main argument
Reflection is the last resort for a small set of genuinely generic problems: encoding libraries (JSON, XML, binary formats), generic pretty-printers, and RPC frameworks that must handle arbitrary message types. The chapter argues that reflection is powerful but costly — in performance, in clarity, and in compile-time safety — and should be used only when there is no other way.
Why reflection exists
Go's type system prevents writing a single function that operates on all types without knowing them in advance. For encoding, for example, json.Marshal must handle int, string, []Foo, map[string]Bar, and any struct — including ones defined after the encoding/json package was written. Reflection provides the runtime type information needed to do this.
reflect.Type and reflect.Value
reflect.TypeOf(x)returns the dynamic type ofxas areflect.Type. Even ifxis stored asinterface{},TypeOfreturns the concrete type.reflect.ValueOf(x)returns the value as areflect.Value, which provides methods to inspect and sometimes modify it regardless of its type.
The Kind system
Rather than infinitely many types, reflect.Value has a finite set of kinds: Bool, Int, Float64, String, Slice, Array, Map, Struct, Ptr, Chan, Func, Interface, Invalid. Code switches on Kind() to handle different categories uniformly, then uses kind-specific methods (Len, Index, Field, MapKeys, Elem).
The Display function
The chapter builds a recursive pretty-printer. A switch on the value's kind handles each case:
- Slices/arrays:
Len()andIndex(i)iterate elements. - Structs:
NumField()andField(i)access fields;Type().Field(i).Namegives the field name. - Maps:
MapKeys()andMapIndex(key)enumerate pairs. - Pointers:
Elem()dereferences;IsNil()checks nil. - Interfaces:
Elem()retrieves the dynamic value.
Addressability and modifying values
Not all reflect.Values can be modified. A value is addressable only if it was obtained by dereferencing a pointer: reflect.ValueOf(&x).Elem(). CanSet() checks both addressability and whether the field is exported. Setting unexported fields panics even if addressable.
Struct field tags
reflect.Type.Field(i).Tag returns the raw struct tag string. Tag.Lookup("json") extracts the value for a specific key. This is how encoding/json reads `json:"name,omitempty"` to determine the JSON field name and omitempty behavior.
Calling methods via reflection
reflect.Value.Method(i) returns a method value. Call(args) invokes it with a []reflect.Value argument slice and returns []reflect.Value results. This is how encoding libraries invoke user-defined MarshalJSON methods without knowing the type at compile time.
Limitations and cautions
- Reflection bypasses compile-time type checking; type mismatches panic at runtime.
- Reflected code is significantly slower than direct code.
- Cyclic data structures cause infinite recursion without explicit cycle detection.
- Unexported fields are inaccessible even through reflection (they cannot be set).
- The authors recommend using reflection sparingly: "clear is better than clever."
Key ideas
reflect.TypeOfandreflect.ValueOfgive runtime access to any value's type and data.Kind()returns a finite category; type-specific methods operate within a category.- Only values obtained by dereferencing pointers are addressable and settable.
- Struct tags are arbitrary metadata attached to fields;
reflectreads them as strings. - Method reflection enables truly generic libraries like
encoding/json. - Reflection trades compile-time safety and performance for generality.
Key takeaway
Reflection is Go's mechanism for the small set of problems that genuinely require operating on arbitrary types at runtime; for everything else, explicit types and interfaces are preferable.
Chapter 13 — Low-Level Programming
Central question
When and how should a Go programmer use the unsafe package to step outside Go's type and memory safety guarantees?
Main argument
Go's type system, bounds checking, and garbage collector make programs safe by default. The unsafe package removes those guarantees for a specific set of low-level operations: querying memory layout, converting between unrelated pointer types, and calling C code via cgo. The chapter argues that these operations are occasionally necessary but should be isolated to the smallest possible code region and approached with the same caution as any C programming.
unsafe.Sizeof, Alignof, Offsetof
These three functions query the memory layout of types at compile time:
unsafe.Sizeof(x)returns the byte size ofx's representation (not the data pointed to). It is a constant expression of typeuintptr, usable as an array dimension or in other constant expressions.unsafe.Alignof(x)returns the required alignment ofx's type in bytes. Numeric types align to their size (up to 8 bytes); all others to the word size.unsafe.Offsetof(x.f)returns the offset in bytes of fieldffrom the start of its struct, accounting for alignment padding ("holes"). This is useful for hand-crafting efficient struct layouts.
These functions are analogous to C's sizeof, alignof, and offsetof macros but are evaluated by the Go compiler with full type-safety context.
unsafe.Pointer
unsafe.Pointer is a special pointer type that can hold the address of any variable. The key operation: an ordinary *T can be converted to unsafe.Pointer, and unsafe.Pointer can be converted back to any other pointer type *U, even if T and U are unrelated. This enables low-level memory tricks that Go's type system otherwise prevents — for example, reinterpreting the bits of a float64 as a uint64 for network serialization.
The uintptr danger
uintptr is an integer type large enough to hold any pointer address. Converting unsafe.Pointer to uintptr creates a "plain number" — the garbage collector does not treat it as a reference. If the GC runs after the conversion and moves the object (on a moving-GC implementation), the uintptr value becomes stale, pointing to freed or re-used memory. The safe rule: never store the result of unsafe.Pointer → uintptr in a variable across a GC safe point. The conversion must be used immediately in the same expression.
Deep equivalence with unsafe.Pointer
The chapter builds an Equal function that compares arbitrary values, including those with cycles. Cycle detection uses a set of (unsafe.Pointer, unsafe.Pointer) pairs to recognize previously compared addresses and avoid infinite recursion. This is more general than reflect.DeepEqual because it treats nil and empty slices/maps as equivalent.
cgo: calling C libraries
cgo is a tool that generates Go bindings for C functions. A Go source file includes C code in a special comment block preceding import "C":
/*
#cgo LDFLAGS: -lbz2
#include <bzlib.h>
*/
import "C"
C types are accessed as C.BZ_RUN, C.bz_stream, etc. Pointer conversion requires unsafe.Pointer. The chapter wraps the libbzip2 compression library: a 938,848-byte input compressed to 335,405 bytes, verified with SHA256 checksums. cgo makes Go programs that call C libraries possible but introduces all the dangers of C memory management at the boundary.
A word of caution
Using unsafe also voids Go's compatibility guarantee with future releases, because it exposes implementation details that may change. The authors advise: restrict unsafe use to the minimum necessary code region, document it carefully, test it with the race detector, and prefer pure-Go alternatives whenever they exist.
Key ideas
unsafe.Sizeof/Alignof/Offsetofquery memory layout as compile-time constants.unsafe.Pointeris the "type-erasure" pointer; it can be cast to and from any*T.- Converting
unsafe.Pointertouintptrloses GC visibility; use immediately, never store. cgogenerates Go bindings for C code; introduces C memory management concerns.unsafeuse voids future compatibility guarantees and should be minimized and documented.- Even with
unsafe, the race detector still works on the surrounding code.
Key takeaway
The unsafe package provides the minimal set of operations needed to interface with hardware, operating systems, and C libraries; every use should be treated as a deliberate exception to Go's safety model, not as a routine tool.
The book's overall argument
- Chapter 1 (Tutorial) — establishes that Go's full power — file I/O, network clients, concurrent servers — is accessible in a handful of lines, motivating why learning its small set of primitives pays dividends immediately.
- Chapter 2 (Program Structure) — lays the foundation: naming rules, declaration forms, type declarations, zero values, and scope; these rules eliminate entire categories of bugs before the first compile.
- Chapter 3 (Basic Data Types) — shows that Go's numeric and string types are precise and explicit: no implicit conversions, no undefined overflow, UTF-8 strings with deliberate rune/byte distinction.
- Chapter 4 (Composite Types) — introduces the four aggregate building blocks (arrays, slices, maps, structs) and their idioms; slices and maps cover 95% of data-structure needs with a minimal API.
- Chapter 5 (Functions) — makes the case that functions are more powerful than they appear: multiple returns enforce error handling, closures enable stateful abstractions, and
deferprovides structured cleanup. - Chapter 6 (Methods) — extends functions to types via explicit receivers; composition through embedding replaces inheritance without requiring class hierarchies.
- Chapter 7 (Interfaces) — introduces Go's sole polymorphism mechanism: implicit satisfaction means interfaces can be defined after implementations, decoupling packages without coupling them to a shared base.
- Chapter 8 (Goroutines and Channels) — presents the CSP model: goroutines are cheap enough to use liberally, channels make coordination explicit, and
selectcomposes multiple streams without callbacks or event loops. - Chapter 9 (Concurrency with Shared Variables) — completes the concurrency picture: when data must be shared rather than communicated, mutexes and
sync.Onceprovide safe access; the race detector catches violations. - Chapter 10 (Packages and the Go Tool) — shows how explicit imports, acyclic dependencies, and globally unique import paths make large-scale builds fast and reproducible without a configuration file.
- Chapter 11 (Testing) — argues that minimal testing conventions — naming, table-driven tests, example functions — produce clearer tests than assertion frameworks, and that
go testintegrates benchmarking and profiling in the same tool. - Chapter 12 (Reflection) — reveals how
encoding/jsonand similar libraries work; demonstrates that the reflect package is a powerful but expensive last resort for genuinely generic code. - Chapter 13 (Low-Level Programming) — closes the abstraction stack by showing how
unsafeandcgobreach the type system when necessary; frames these as deliberate escapes from Go's safety model, not everyday tools.
Common misunderstandings
Misunderstanding: goroutines are threads
Goroutines are not OS threads. They are lightweight, user-space units of execution multiplexed onto a small number of OS threads by the Go runtime. A program may create millions of goroutines where creating millions of threads would exhaust system memory. The runtime scheduler manages blocking, parking, and resuming goroutines transparently.
Misunderstanding: channels are queues
Channels are synchronization points, not buffers. An unbuffered channel blocks the sender until a receiver is ready — it has no capacity and stores no values. Buffered channels add a queue, but even a buffered channel with capacity 100 blocks the sender when the queue is full. Using channels as general-purpose message queues without understanding back-pressure leads to goroutine leaks.
Misunderstanding: interface embedding is inheritance
When a struct embeds another type, it gains the embedded type's fields and methods through promotion, but it does not "become" the embedded type. A ColoredPoint embedding a Point does not satisfy interfaces that *Point satisfies unless ColoredPoint independently implements those methods. This is composition, not subtyping.
Misunderstanding: a nil interface value equals a nil pointer
An interface value is nil only when its dynamic type is nil. An interface holding a (*os.File)(nil) is not nil — it has a non-nil dynamic type — and will not compare equal to nil. This is a common source of bugs when returning errors: returning a typed nil pointer wrapped in an error interface returns a non-nil error value.
Misunderstanding: defer is expensive
The book shows that modern Go implementations make defer inexpensive enough for routine use — opening a file and immediately deferring its close is idiomatic and correct, not a performance concern. Deferred calls inside tight inner loops may accumulate and warrant refactoring, but this is a rare case.
Misunderstanding: reflection is general-purpose metaprogramming
Reflection is not a substitute for generics or for normal polymorphism. It bypasses compile-time type checking, runs significantly slower than direct code, and produces code that is harder to read and maintain. Its legitimate uses are narrow: encoding/decoding, test frameworks, and RPC stubs that must handle types unknown at compile time.
Central paradox / key insight
Go achieves expressiveness through omission. Every mainstream language adds features to become more powerful — Go deliberately removes them. No inheritance, no exceptions, no default parameters, no operator overloading, no generics (in the first edition). Yet programs written in Go are often clearer, faster to compile, and easier to maintain than equivalent programs in feature-rich languages.
The key insight is that the hardest problems in large-scale software — making programs safe to run concurrently, making dependencies manageable at scale, making code readable by someone who did not write it — are made harder, not easier, by features. Goroutines and channels solve concurrency at scale not by adding a richer type system for threads but by making the communication topology explicit. Interfaces enable polymorphism across package boundaries without requiring a shared class hierarchy. The package system scales to millions of lines because imports are explicit and acyclic.
"Go has no particular ancestor. ... The language is in some sense a reaction to the complexity that had accumulated in mainstream languages over the previous decade." — Preface
The paradox is that a language designed by simplification can handle problems of greater complexity than languages designed by accumulation.
Important concepts
goroutine
A lightweight concurrent function execution, launched with go f(). Managed by the Go runtime, not the OS; starts at ~2 KB stack and grows on demand. Many goroutines multiplex onto a small number of OS threads (m:n scheduling).
channel
A typed conduit for passing values between goroutines. Created with make(chan T) (unbuffered) or make(chan T, n) (buffered with capacity n). Send (ch <- v) and receive (v := <-ch) synchronize goroutines. Closing (close(ch)) broadcasts completion.
interface
A named set of method signatures. A concrete type satisfies an interface implicitly by implementing all its methods. Interface values carry a (dynamic type, dynamic value) pair; a nil interface is distinct from a non-nil interface holding a nil pointer.
goroutine leak
A goroutine that is permanently blocked because no other goroutine will ever receive from or send to its channel, or because its blocking condition will never become true. Leaking goroutines consume memory and other resources indefinitely.
data race
Concurrent access to a variable where at least one access is a write and the accesses are not protected by synchronization. Results in undefined behavior. Detected at runtime by the -race flag.
select
A control structure that waits for the first ready channel operation among multiple cases, choosing randomly when multiple are ready simultaneously. The default case makes it non-blocking.
defer
A statement that schedules a function call to execute when the enclosing function returns, in LIFO order, regardless of whether the return is normal or due to panic. Used for resource cleanup, tracing, and modifying named return values.
GOMAXPROCS
The maximum number of OS threads that execute Go code simultaneously. Defaults to the number of logical CPUs. Goroutines blocked on system calls or I/O do not count against this limit.
unsafe.Pointer
A special pointer type that can hold the address of any value and be cast to any other pointer type. Used to perform memory layout tricks and pass Go pointers to C code. Bypasses the type system; use sparingly.
cgo
A tool that generates Go bindings for C libraries. Allows Go programs to call C functions directly by embedding C code and declarations in special comment blocks. Introduces C memory management complexity at the Go-C boundary.
blank identifier
The underscore _ is used to discard values: the unused import import _ "image/png" and the discarded loop index for _, v := range s. Go requires all declared variables and imports to be used; _ satisfies this requirement when a value is intentionally unused.
struct embedding
Placing a type inside a struct without a field name causes the embedded type's fields and methods to be promoted to the outer struct. This is Go's composition mechanism; it is not inheritance and does not imply subtype relationship.
method set
The set of methods associated with a type. The method set of *T includes all methods of T plus all methods with pointer receiver *T. Interface satisfaction is determined by method sets.
iota
A predeclared identifier that generates sequential integer constants inside a const block, starting at 0 and incrementing by 1 per constant. Used for enumerations and bit-flag constants.
References and Web Links
Primary book and edition information
- Donovan, Alan A. A., and Brian W. Kernighan. The Go Programming Language. Addison-Wesley Professional Computing Series, 2015. ISBN 978-0134190440.
Background and overview
Chapter-by-chapter study notes
- Shichao's Notes — full GOPL chapter notes (Chapters 1–12)
- Chapter 1 — Tutorial
- Chapter 2 — Program Structure
- Chapter 3 — Basic Data Types
- Chapter 4 — Composite Types
- Chapter 5 — Functions
- Chapter 6 — Methods
- Chapter 7 — Interfaces
- Chapter 8 — Goroutines and Channels
- Chapter 9 — Concurrency with Shared Variables
- Chapter 10 — Packages and the Go Tool
- Chapter 11 — Testing
- Chapter 12 — Reflection
Chapter 13 — Low-Level Programming sources
- O'Reilly section 13.1: unsafe.Sizeof, Alignof, Offsetof
- Low-Level Programming chapter overview (apprize.best)
- Go unsafe package official documentation
Exercise solutions and code examples
- Official source code at gopl.io (go get gopl.io/...)
- GitHub — ptdecker/gobasics: worked examples
- GitHub — patrickbucher/gopl.io: exercises and examples
- GitHub — torbiak/gopl: exercise solutions
Additional chapter summaries and study resources
These are secondary summaries and should be used alongside, rather than instead of, the original book.