Skip to content

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.

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 error as 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.Reader promises to work with anything that satisfies it; a function that accepts *os.File has 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.

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.

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.Context that gets cancelled — and does it actually check ctx.Done()?
  • Is it tracked by a sync.WaitGroup, and is Add called before the goroutine starts, from the same goroutine that will call Wait?
  • If it panics, is there a recover reachable from inside that same goroutine — a recover anywhere 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.

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 defer placed 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)
}
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 one
3. Goroutines → what stops each `go` statement, and who waits for it
4. 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.
What you getBenefitCost
Reading signatures firstCatches contract violations before you trace a single branchRequires knowing what context.Context, pointer receivers, and error returns actually promise
Tracing the whole error pathFinds the swallowed/duplicated/leaking errors AI code hidesSlower than reading only the happy path
Tracking goroutine lifetimes explicitlyFinds leaks and lost panics before they page someone at 3amRequires holding more state in your head while reading
Checking ownership on every acquireFinds resource leaks and use-after-close bugsTedious on code with many small helper functions
  • “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.
You see func Save(ctx context.Context, r io.Reader) (*Record, error) before reading the body. What have you already learned?
Why is the error path usually more important to read carefully than the happy path?
For a goroutine started with go worker(ctx), which question matters most when reviewing it?
What should a reviewer check next about every caller of open?
func open() (*os.File, error) {
    f, err := os.Open("data.txt")
    return f, err
}