How to read Go you did not write
Every earlier module in this course taught you to write Go. This module teaches the other half of the job the course promised from the start: reading Go someone — or something — else wrote, and deciding whether it is correct. An AI assistant will hand you code that compiles, runs, and looks exactly like the idiomatic examples you have been studying. Compiling and running are not the same as correct. The four sections below are the order experienced Go reviewers actually read code in, and every later lesson in this module drills one part of it.
Read types and signatures first
Section titled “Read types and signatures first”Before you read a single line of a function’s body, read its signature. In Go the signature is not decoration — it is a contract the compiler enforces, and it already answers most of the questions you would otherwise have to trace through the body to find:
- Does the first parameter take a
context.Context? The call is cancellable and may be running concurrently with something that can time it out. - Is the receiver a pointer (
*T) or a value (T)? A pointer receiver can mutate the thing you passed in; a value receiver cannot — it works on a copy. - Does it return an
erroras the last value? Something in that function can fail, and Go’s convention puts the failure signal in a place you cannot accidentally destructure away. - Are the parameter and return types concrete or interfaces? A function that accepts
io.Readerpromises to work with anything that satisfies it; a function that accepts*os.Filehas narrowed its own usefulness, on purpose or by accident.
None of this requires running the code. A reviewer who reads func (a *Account) Withdraw(ctx context.Context, amount int) error before looking at the body already knows: this mutates a, it can be cancelled, and it can fail — so the next thing to check is whether every caller respects all three.
Follow the error path
Section titled “Follow the error path”Go’s if err != nil convention means the error path is usually the majority of the lines in a function, and it is also the path AI-generated code gets subtly wrong most often — because the happy path is what training data rewards looking correct. Reading a function well means tracing every return that carries a non-nil error, not just the final one:
- Where does the error come from — a stdlib call, a wrapped lower-level error, a sentinel comparison?
- Is it returned, logged, or both? Handling it twice (logging and returning) usually means the caller will log it again.
- Does an early return leak a resource that a later line would have closed?
- Is a zero value returned alongside the error, and does any caller use that zero value without checking the error first?
The lesson that explains the mechanics behind these questions is Error Values and Wrapping Errors — this module assumes you know %w, errors.Is, and errors.As, and instead trains you to spot where AI code violates them.
Follow goroutine lifetimes
Section titled “Follow goroutine lifetimes”Every go keyword starts something whose lifetime you now have to track independently of the function that launched it. Reading concurrent code means answering, for every goroutine: what makes it stop, and who is waiting for it to stop?
- Does it read from or write to a channel that might never be ready — a leak waiting to happen?
- Does it hold a
context.Contextthat gets cancelled — and does it actually checkctx.Done()? - Is it tracked by a
sync.WaitGroup, and isAddcalled before the goroutine starts, from the same goroutine that will callWait? - If it panics, is there a
recoverreachable from inside that same goroutine — arecoveranywhere else does not help it?
Goroutines, Channels, and The sync package cover the semantics; this module’s catalog shows exactly how AI-generated code breaks each rule.
Follow resource ownership
Section titled “Follow resource ownership”Anything with a Close, Unlock, or Done method has an owner, and Go never enforces who that is at compile time — the discipline is entirely on the code you are reading. For every acquired resource, ask:
- Who calls the matching release — the acquirer, or something the acquirer hands the resource to?
- Is the release guaranteed on every path, including early returns and panics — usually via
deferplaced immediately after the acquire succeeds? - Can the resource be used after it is released — a closed channel received from, a file read after
Close, a mutex touched after the struct holding it was copied?
package main
import ( "context" "errors" "fmt")
// The signature alone tells you three things before you read a line of the// body: ctx first means the call is cancellable, the pointer receiver means// Withdraw mutates the Account, and the error return means something here// can fail — go find out what before you trust a caller that ignores it.type Account struct { Balance int}
func (a *Account) Withdraw(ctx context.Context, amount int) error { if err := ctx.Err(); err != nil { return err // follow the error path: cancellation short-circuits the withdrawal } if amount > a.Balance { return errors.New("insufficient funds") } a.Balance -= amount return nil}
func main() { acc := &Account{Balance: 100} ctx := context.Background()
if err := acc.Withdraw(ctx, 30); err != nil { fmt.Println("withdraw failed:", err) } fmt.Println("balance after first withdraw:", acc.Balance)
if err := acc.Withdraw(ctx, 1000); err != nil { fmt.Println("withdraw failed:", err) // easy branch to miss when skimming the happy path } fmt.Println("balance after second withdraw:", acc.Balance)} Mental Model
Section titled “Mental Model”Reading order for a function you did not write:
1. Signature → ctx? pointer or value receiver? error return? interface or concrete params?2. Error path → every non-nil return, not just the last one3. Goroutines → what stops each `go` statement, and who waits for it4. Ownership → who calls Close/Unlock/cancel, and is it guaranteed on every path
Skipping straight to "does it compile and run" answers none of these.Trade-offs
Section titled “Trade-offs”| What you get | Benefit | Cost |
|---|---|---|
| Reading signatures first | Catches contract violations before you trace a single branch | Requires knowing what context.Context, pointer receivers, and error returns actually promise |
| Tracing the whole error path | Finds the swallowed/duplicated/leaking errors AI code hides | Slower than reading only the happy path |
| Tracking goroutine lifetimes explicitly | Finds leaks and lost panics before they page someone at 3am | Requires holding more state in your head while reading |
| Checking ownership on every acquire | Finds resource leaks and use-after-close bugs | Tedious on code with many small helper functions |
Common Misconceptions
Section titled “Common Misconceptions”- “It compiled and the output looked right, so it’s correct.” Every pattern in this module’s catalog compiles and produces plausible output on its happy path — that is exactly what makes it dangerous.
- “Reading the happy path is reading the function.” In idiomatic Go the error-handling lines usually outnumber the happy-path lines; skipping them is skipping most of the function.
- “If a human wrote it, it’s fine; only AI code needs this scrutiny.” Every pattern here also appears in human-written Go — AI just produces it at a much higher and more confident-looking volume.
- “A code reviewer’s job is to find bugs by running the code.” Most of the bugs in this module’s catalog never crash on the input you happen to try; they crash later, under load, or under cancellation.