# Learn Go > Free Go tutorials, plus why Go is the highest-leverage language for agent-assisted development. Canonical: https://learn-go.org/ Licence: content free to read and quote with attribution to Learn Go (https://learn-go.org/). Maintainer: Code Learning Dojo. Last built 2026-09-06. ## Foundations The syntax and the mental model. Short, runnable, no fluff. - [Hello, World!](https://learn-go.org/hello-world/): Welcome to the first tutorial. In this tutorial you will learn how to write your first line of code. - [Variables](https://learn-go.org/variables/): A variable is a name given to a storage area that the programs can manipulate. - [Arrays](https://learn-go.org/arrays/): Arrays are essentially storage spaces that can be filled with as much data as one would like. Variables, unlike arrays, can only contain one piece of data. - [Conditions and Switch](https://learn-go.org/conditions/): Go has one conditional statement and one switch, and both do more than their equivalents elsewhere. - [Loops](https://learn-go.org/loops/): Go has only one looping construct, the for loop. - [Functions](https://learn-go.org/functions/): Multiple return values, named results, variadics, closures — and the error convention that shapes every Go API. - [Slices and maps](https://learn-go.org/slices-and-maps/): The two data structures you will use constantly, and the aliasing behaviour that surprises everyone once. - [Structs and interfaces](https://learn-go.org/structs-and-interfaces/): Go has no classes and no inheritance. It has structs, methods and implicitly satisfied interfaces — which turns out to be enough. - [Errors](https://learn-go.org/errors/): Go's most distinctive design decision. Verbose, explicit, and the reason generated Go rarely swallows a failure. - [Methods and Pointers](https://learn-go.org/methods-and-pointers/): Pointers in Go are simple by design: no arithmetic, no manual freeing. The one decision you make repeatedly is value receiver or pointer receiver. - [Goroutines and channels](https://learn-go.org/concurrency/): Go's headline feature, and the one place where the compiler stops protecting you. - [Strings and Runes](https://learn-go.org/strings-and-runes/): A Go string is a read-only slice of bytes, not characters. Understanding that distinction prevents a specific and common category of bug. - [Packages and Modules](https://learn-go.org/packages-and-modules/): How Go code is organised, why a capital letter is the only access modifier you get, and what go.mod actually does. - [Defer, Panic and Recover](https://learn-go.org/defer-panic-recover/): Go's cleanup mechanism, its crash mechanism, and the narrow set of cases where catching a crash is the right thing to do. - [Testing](https://learn-go.org/testing/): Testing ships with the language. Table-driven tests, subtests, benchmarks and the race detector all come from one command. - [JSON and File I/O](https://learn-go.org/json-and-io/): Struct tags, the reader and writer interfaces that unify all I/O in Go, and the capitalisation mistake that silently drops your fields. - [Generics](https://learn-go.org/generics/): Type parameters arrived late to Go and the community uses them sparingly. Here is the everyday use, and why restraint is the right default. ## AI-Native Configuring agents, harnesses and feedback loops for this language. Updated as the tooling moves. - [Why Go is the best language for agent-assisted development](https://learn-go.org/ai/why-go/): Small spec, explicit errors, one formatter, a compiler that refuses to look away. Every property that made Go boring makes it exceptional to hand to a machine. - [Writing an AGENTS.md for Go](https://learn-go.org/ai/agents-md/): Go needs a shorter instructions file than any other language, because the compiler already enforces most of what you would otherwise write down. - [Testing Go that calls a language model](https://learn-go.org/ai/evals/): Go is increasingly the language the AI infrastructure is written in rather than the AI feature. That changes what you test — and interfaces plus httptest make it unusually pleasant. - [Tracking and cutting token costs in Go](https://learn-go.org/ai/tokenomics/): Go is usually the thing in front of the model rather than the thing calling it — which makes it the right place to enforce budgets, cancel abandoned work and expose the metrics. ## Review & Verify How generated code fails in this language, and the checks that catch it before your users do. - [The Go mistakes language models actually make](https://learn-go.org/review/failure-modes/): The compiler catches most of it. This is the list of what gets through — almost all of it concurrency. - [Dependency hygiene for Go modules](https://learn-go.org/review/dependencies/): Go's module system already solves most of what goes wrong elsewhere. What is left is hallucinated import paths, and the handful of directives that quietly disable the guarantees. - [Security review checklist for AI-generated Go](https://learn-go.org/review/security/): Go's standard library gets most things right by default, which means the vulnerabilities that survive are the ones where you chose the wrong stdlib package. - [The performance traps in generated Go](https://learn-go.org/review/performance/): Go's performance problems are almost all allocations and unbounded concurrency — and the toolchain will tell you about both if you ask it. ## Reference pages - [About Learn Go, and how we make money](https://learn-go.org/about/): Editorial policy, sourcing, corrections and affiliate disclosure for Learn Go, part of the Code Learning Dojo network. - [The Go stack we would set up today](https://learn-go.org/tools/): An opinionated Go toolchain for 2026: golangci-lint, staticcheck, goleak, the race detector, editors, and hosting — plus what to skip. --- # Full text ## Hello, World! Source: https://learn-go.org/hello-world/ Welcome to the first tutorial. In this tutorial you will learn how to write your first line of code. ## The Go mistakes language models actually make Source: https://learn-go.org/review/failure-modes/ Go's compiler eliminates most of what makes generated code in other languages risky. What survives clusters almost entirely in one area — concurrency — plus a handful of idiom mistakes that come from training data older than the current standard library. That is a short list, which is the good news. It is also a list of the hardest bugs to find, which is the bad news. ## Concurrency ### 1. Goroutine leaks ```go func fetchAll(urls []string) []Result { ch := make(chan Result) for _, u := range urls { go func(u string) { ch <- fetch(u) }(u) // blocks forever if nobody reads } var out []Result for i := 0; i < 3; i++ { // reads 3, started len(urls) out = append(out, <-ch) } return out } ``` Every unread goroutine blocks on the send and never exits. Under load the process accumulates goroutines until it dies. Nothing fails in a test. **Correct:** buffer the channel to the number of sends, or use `errgroup` with a context, and always have a way for a goroutine to be cancelled. **Catch it with:** `go.uber.org/goleak` in `TestMain`. Three lines, and it turns every leak into a test failure. ```go func TestMain(m *testing.M) { goleak.VerifyTestMain(m) } ``` ### 2. Unbuffered channel deadlock ```go ch := make(chan int) ch <- 1 // deadlock: nothing is receiving yet fmt.Println(<-ch) ``` Generated code frequently reaches for an unbuffered channel where a buffered one or a `sync.WaitGroup` is meant. **Catch it with:** `go test -race` and a timeout — `go test -timeout 30s` turns a deadlock into a failure with a full goroutine dump. ### 3. WaitGroup misuse `wg.Add(1)` inside the goroutine instead of before it; `defer wg.Done()` missing; passing a `WaitGroup` by value so each goroutine gets a copy. All three appear regularly and all three produce a program that either exits early or hangs. ### 4. Unsynchronised shared state ```go counter := 0 for i := 0; i < 100; i++ { go func() { counter++ }() // data race } ``` **Catch it with:** `go test -race`. Always. Put it in `make check` and never take it out — the race detector is the single highest-value tool in Go and it finds this class of bug reliably. ### 5. Context not propagated, or ignored ```go func (s *Service) Get(ctx context.Context, id string) (*User, error) { return s.db.Query("SELECT ...") // ctx dropped; no cancellation, no timeout } ``` The generated function accepts a context because the signature convention says so, then never uses it. Cancellation and timeouts silently stop working, and a request that the client abandoned keeps a database connection busy. **Catch it with:** `go vet`'s `lostcancel`, plus `contextcheck` in `golangci-lint`, plus grep: `ctx context.Context` in a signature with no `ctx` in the body is always worth a look. ## Errors ### 6. Wrapping with `%v` instead of `%w` ```go return fmt.Errorf("load user: %v", err) // breaks errors.Is / errors.As ``` The error message looks identical, so nothing appears wrong until a caller's `errors.Is(err, sql.ErrNoRows)` stops matching. **Catch it with:** `errorlint` in `golangci-lint`. ### 7. Errors ignored with `_` Explicit, greppable, and still generated — most often on `defer f.Close()` for a writable file, where the close error is the one that tells you the write failed. ```bash git diff | grep -nE '_ = |_, _ =' ``` **Catch it with:** `errcheck` (included in `golangci-lint`). ### 8. Sentinel errors compared with `==` `if err == ErrNotFound` fails as soon as anything in the chain wraps it. Use `errors.Is`. `errorlint` catches this too. ## Types and nil ### 9. The nil interface trap ```go func find() error { var e *MyError = nil return e // non-nil interface holding a nil pointer } if find() != nil { /* this runs */ } ``` The canonical Go gotcha, well represented in training data as an *example of a bug* — which means it also gets reproduced as code. **Correct:** return `nil` literally, never a typed nil pointer. ### 10. Slice aliasing after `append` ```go a := []int{1, 2, 3, 4, 5} b := a[:2] b = append(b, 99) // overwrites a[2] — b shares a's backing array ``` Generated slicing code assumes copies. Use `slices.Clone`, or a full slice expression `a[:2:2]` to force reallocation. ### 11. Map iteration order Deliberately randomised in Go. Generated code that builds output by ranging a map produces results that differ between runs — flaky tests, non-reproducible files. Sort the keys. ## Stale idioms Models trained on older Go write older Go. None of these are wrong, but they are worth correcting in `AGENTS.md` if your codebase has moved on. | Generated | Current | |---|---| | `ioutil.ReadFile` | `os.ReadFile` (ioutil deprecated since 1.16) | | `interface{}` | `any` | | `rand.Seed(time.Now()...)` | unnecessary since 1.20; `math/rand/v2` | | hand-rolled `min`/`max`/`contains` | builtins and `slices` package | | `x := x` loop shadowing | unnecessary since 1.22 | | `logrus`, `zap` by reflex | `log/slog` is stdlib | | `github.com/pkg/errors` | `fmt.Errorf` with `%w` | ## The config that catches most of this ```yaml .golangci.yml linters: enable: - errcheck # ignored errors - errorlint # %w, errors.Is - govet # lostcancel, copylocks, printf - staticcheck # the big one - contextcheck # dropped contexts - bodyclose # unclosed response bodies - rowserrcheck - noctx # http requests without a context - gosec ``` ```makefile check: gofmt -l -w . golangci-lint run go test -race -count=1 -timeout 60s ./... ``` `-count=1` disables test caching, which matters when an agent is iterating — a cached pass is not a pass. :::verdict The short version In Go, the compiler and `golangci-lint` handle nearly everything except concurrency. So put `-race` and `goleak` in your default test command and spend your review attention on the goroutines. That is where the bugs are. ::: :::promo hetzner ::: ## Common questions ### Is generated Go safer than generated Python? For everything the compiler and vet can see, substantially — unused variables, type confusion, unhandled imports, silently swallowed errors are all structurally harder. Concurrency is the exception, and it is exactly where Go's own difficulty is concentrated, so the residual risk is narrow but sharp. ### Do I really need `-race` on every test run? Yes. It roughly doubles test time and finds a class of bug that is otherwise found in production, at 3am, non-reproducibly. If your suite is too slow with it, that is an argument for a faster suite rather than for turning it off. ### Why does generated Go use deprecated stdlib functions? Because `ioutil.ReadFile` appears in a decade of training data and `os.ReadFile` in three years of it. It is harmless as such — the functions still work — but it is a useful signal about how current the rest of the generated code is likely to be. A line in `AGENTS.md` fixes it. ## Variables Source: https://learn-go.org/variables/ A variable is a name given to a storage area that the programs can manipulate. The name of a variable can be composed of letters, digits, and the underscore character. It must begin with either a letter or an underscore. ### Numbers They are arithmetic types and they represents following values throughout the program. a) integer types b) floating point To define an integer, use the following syntax: ```go var a int = 4 var b, c int b = 5 c = 10 fmt.Println(a) fmt.Println(b + c) ``` To define a floating point number, you may use one of the following notations: ```go var d float64 = 9.14 fmt.Println(d) ``` ### Strings Strings in Go are defined with double quotes. ```go var s string = "This is string s" fmt.Println(s) ``` The difference between the two is that using double quotes makes it easy to include apostrophes (whereas these would terminate the string if using single quotes) ```go var s string = "Don't worry about apostrophes" fmt.Println(s) ``` ### Shorthand Declaration The `:=` notation serves both as a declaration and as initialization. `foo := "bar"` is equivalent to `var foo string = "bar"` ```go a := 9 b := "golang" c := 4.17 d := false e := "Hello" f := `Do you like golang, so far?` g := 'M' fmt.Println(a) fmt.Println(b) fmt.Println(c) fmt.Println(d) fmt.Println(e) fmt.Println(f) fmt.Println(g) ``` ## Why Go is the best language for agent-assisted development Source: https://learn-go.org/ai/why-go/ There is a real, structural reason Go punches above its weight in agentic development, and it is not that models are especially good at Go. It is that Go removes most of the ways generated code goes wrong before you ever see it. ## The compiler is a feedback loop that cannot be argued with Every property below matters for one reason: an agent is a loop that needs honest, fast feedback. Go's compiler is the strictest cheap signal in mainstream programming. - **Unused variables are errors.** Not warnings. Dead code from an abandoned approach does not compile. - **Unused imports are errors.** The single most common cosmetic mess in generated Python and JavaScript simply cannot exist here. - **No implicit conversions.** The class of bug where a string quietly becomes a number is gone. - **Compilation is fast enough to run on every edit.** A second or two on a real codebase. That means the agent gets the signal immediately, not at the end. ```bash go build ./... && go vet ./... && go test ./... ``` Three commands, a few seconds, and an enormous fraction of what could be wrong is already ruled out. Compare with the equivalent effort needed to get a Python codebase to the same level of assurance. ## Explicit errors remove the most common generated bug The single most reliable failure in generated code across every language is the silently swallowed error. In Python it is `except Exception: pass`. In JavaScript it is an unawaited promise. In Go, ignoring an error requires writing `_` — a visible, greppable, reviewable act. ```go data, err := os.ReadFile(path) if err != nil { return fmt.Errorf("read config: %w", err) } ``` There is nowhere for the failure to hide. And because the pattern is so uniform, an agent produces it correctly essentially every time — there is only one way to write it. ```bash # your entire "did it swallow an error" review, as one command git diff | grep -nE '_ = |_, _ =|if err != nil \{\s*\}' ``` ## One formatting, no debate `gofmt` means style is not a decision, so it is not a source of diff noise, so your review is entirely about substance. Every generated file already matches every other file in the repo. Nobody has ever configured this. This sounds trivial and is not: a large share of review fatigue in other languages comes from diffs where the meaningful change is buried in formatting churn. ## A small specification fits in the model's head Go has 25 keywords and a specification a person can read in an afternoon. The practical consequence is that generated Go rarely uses an obscure feature incorrectly, because there are very few obscure features. There is one loop construct. There is no inheritance. There are no decorators, metaclasses, operator overloading, or three competing async models. Expressive languages give a model more ways to be clever, and clever is exactly what you do not want in code you have to review at volume. :::verdict The counterintuitive bit The features Go is criticised for lacking are the same ones that produce the hardest generated code to review. Verbosity is a cost you pay once, at writing time — which is now the cheap part. ::: ## The standard library reduces dependency risk Go's standard library covers HTTP servers and clients, JSON, TLS, templating, cryptography, testing and much more. That materially reduces how often an agent reaches for a third-party package — which is the moment [hallucinated package names](/review/failure-modes/) and supply-chain risk enter. The module system helps too: `go.mod` and `go.sum` are automatic, checksummed, and `go mod tidy` is deterministic. ## The setup, in full ```makefile Makefile .PHONY: check test lint check: lint test lint: gofmt -l -w . go vet ./... staticcheck ./... test: go test -race -count=1 ./... ``` ```markdown AGENTS.md Go 1.23. Standard layout: cmd/, internal/, pkg/. ## Commands - Everything: `make check` (gofmt, vet, staticcheck, go test -race) - One test: `go test -run TestName ./internal/pkg -v` ## Conventions - Errors wrap with %w and context: `fmt.Errorf("load user %s: %w", id, err)`. - No naked returns. No panics outside main() and package init. - Contexts are the first parameter and are actually plumbed through. - Table-driven tests. Subtests with t.Run. - Interfaces are defined by the consumer, in the consumer's package. ## Landmines - internal/scheduler is leader-elected. Changing tick timing needs an ops review. ``` `-race` is doing a lot of work in that Makefile. Concurrency is where generated Go is genuinely weakest — see [the failure modes](/review/failure-modes/) — and the race detector is the check that catches it. The instructions file above is abbreviated; the full version, and why Go’s should be the shortest one you write, is in [writing an AGENTS.md for Go](/ai/agents-md/). :::promo boot-dev ::: ## Where Go is still weak for this Being honest about the other side: - **Concurrency.** Goroutine leaks, unbuffered-channel deadlocks and captured loop variables are the one area where generated Go is regularly wrong, and the compiler does not help. `-race` and `goleak` do. - **Generics.** Introduced recently enough that training data is thin. Generated generic code is often over-complicated. Push back towards concrete types. - **Newer stdlib.** `log/slog`, `math/rand/v2`, the 1.22 loop variable change. Models frequently write the older idiom. Worth a line in `AGENTS.md`. ## Common questions ### Does this mean I should rewrite my Python service in Go? No. Language choice should follow the problem, the ecosystem and your team. The claim here is narrower: *if you are already choosing between them for a networked service, the agent-assisted development story is a real point in Go's favour* and it did not used to be on the list. ### Is Go's verbosity still a downside if a machine writes it? The writing cost mostly disappears; the reading cost does not, and reading is now the bottleneck. Go's verbosity is the kind that makes control flow explicit rather than the kind that hides it, which is the useful sort when you are reviewing volume. ### What about the loop variable change in Go 1.22? Per-iteration loop variables fixed a genuine footgun, and models trained on older code still sometimes write the `x := x` shadowing workaround. It is harmless, just noise — but if you see generated code relying on the *old* sharing behaviour, that is a real bug. ## Arrays Source: https://learn-go.org/arrays/ ## Arrays Arrays are essentially storage spaces that can be filled with as much data as one would like. Variables, unlike arrays, can only contain one piece of data. Now there are some caveats. For instance, an array is syntactically created using one data type, just like variables. Yet, an array grants ease of access and far more capabilities when considering large/vast amounts of data compared to a variable(single storage space/value). ## Examples ```go // An array named favNums filled with 3 integers var favNums[3] int // Insert data into the array // The first storage space will be assigned the value of 1. favNums[0] = 1 // The second storage space will be assigned the value of 2. favNums[1] = 2 // The third and final storage space will be assigned the value of 3. favNums[2] = 3 ``` An alternative syntax to the creation of arrays in golang is: ```go favNums := [4] int {50, 25, 30, 33} ``` In order to access members of an array, reference the storage space’s address or number you used to create it. ```go fmt.Println(favNums[0]) ``` output: ```go 50 ``` ## Conditions and Switch Source: https://learn-go.org/conditions/ ```go if score > 90 { fmt.Println("excellent") } else if score > 70 { fmt.Println("good") } else { fmt.Println("keep going") } ``` No parentheses around the condition, and the braces are mandatory even for one line. The opening brace must be on the same line — Go's automatic semicolon insertion breaks the statement otherwise, which is why `gofmt` will not let you move it. ## The statement-scoped if The idiom you will write most often in Go: ```go if err := doSomething(); err != nil { return fmt.Errorf("do something: %w", err) } // err does not exist here ``` The initialiser runs first, then the condition. Anything it declares is scoped to the `if` and its `else` branches — which keeps short-lived variables like `err` out of the enclosing function. ```go if user, err := findUser(id); err != nil { return err } else { fmt.Println(user.Name) // both user and err visible here } ``` In practice you rarely need the `else` — returning early is the dominant Go style: ```go user, err := findUser(id) if err != nil { return err } fmt.Println(user.Name) // the happy path stays unindented ``` ## switch Go's switch has no automatic fallthrough, so no `break` is needed: ```go switch status { case "pending": fmt.Println("waiting") case "shipped", "delivered": // several values in one case fmt.Println("on its way") default: fmt.Println("unknown") } ``` ### switch with no condition A `switch` with nothing after it means `switch true`, which replaces a long if/else chain: ```go switch { case score > 90: grade = "A" case score > 70: grade = "B" default: grade = "C" } ``` This is idiomatic Go and usually reads better than the equivalent `else if` ladder. ### switch with an initialiser ```go switch hour := time.Now().Hour(); { case hour < 12: fmt.Println("morning") case hour < 18: fmt.Println("afternoon") default: fmt.Println("evening") } ``` ### fallthrough Available, and rare: ```go switch n { case 1: fmt.Println("one") fallthrough // continue into the next case regardless of its condition case 2: fmt.Println("two") } // n == 1 prints both ``` ## Type switches A switch on the dynamic type of an interface value. This is how you handle a value that could be several things: ```go func describe(v any) string { switch x := v.(type) { case nil: return "nothing" case int: return fmt.Sprintf("int %d", x*2) // x is an int here case string: return "string of length " + strconv.Itoa(len(x)) case []int: return fmt.Sprintf("%d ints", len(x)) case error: return "error: " + x.Error() default: return fmt.Sprintf("unhandled %T", x) } } ``` Inside each case, `x` has that specific type. `%T` in the default branch prints the actual type, which is invaluable when debugging. ## Comparison and logic ```go == != < <= > >= && || ! ``` `&&` and `||` short-circuit. Two Go-specific points: **There is no ternary operator.** Write the `if`, or a small helper: ```go label := "off" if enabled { label = "on" } ``` **There are no truthy values.** A condition must be a `bool`: ```go if count { } // error: non-boolean condition if count != 0 { } // fine if name != "" { } // fine ``` That strictness removes a whole class of bug found in dynamically typed languages — and it is one reason generated Go is less likely to be subtly wrong than generated JavaScript. ## Exercise ```go package main import "fmt" func main() { scores := []int{95, 72, 58, 88, 100} // For each score print ": " where grade is // A for 90+, B for 80-89, C for 70-79, F below 70. // Use a conditionless switch. fmt.Println(scores) } ``` ## Common questions ### Why is there no ternary operator? A deliberate omission — the Go authors judged that nested ternaries hurt readability more than the brevity helps. The three-line `if` is the intended form, and in practice it is rarely the thing that makes Go verbose. ### When should I use a type switch? When handling a value whose type genuinely varies — parsing arbitrary JSON, walking an AST, or inspecting an error chain. If you find yourself type-switching on your own types regularly, an interface with a method is usually the better design. ### Should I use `else`? Sparingly. The dominant Go style is to handle the error case and return early, leaving the happy path unindented. A function whose main logic sits inside an `else` block is usually one early return away from being clearer. ## Dependency hygiene for Go modules Source: https://learn-go.org/review/dependencies/ Go has the best default supply-chain story of any language on this network, and it is worth knowing why before looking at what can still go wrong. - **`go.sum` is mandatory and checked.** Every module version is pinned by hash and verified on every build. - **The checksum database** cross-checks those hashes against a public transparency log, so a module author cannot quietly change a published version. - **Minimal version selection** means you get the lowest version that satisfies all requirements, not the newest. Builds do not change under you because someone published a release. - **`govulncheck` does reachability analysis**, reporting only vulnerabilities your code can actually reach. That removes most of the attack surface that [dominates npm and PyPI](https://learn-python.com/review/dependencies/). What is left is smaller and more specific. ## Hallucinated import paths Models invent module paths, and Go's are compositional enough to invent convincingly: ```go import ( "github.com/gorilla/websocket" // real "github.com/gin-gonic/gin/middleware" // plausible subpackage, does not exist "github.com/uber-go/zap" // wrong path: it is go.uber.org/zap "golang.org/x/exp/maps" // moved into the stdlib; may not be what you want ) ``` Three flavours worth recognising: - **Wrong vanity path.** `go.uber.org/zap`, not `github.com/uber-go/zap`. Both look right. - **Invented subpackage.** The module exists; that path within it does not. - **Stale location.** A package that moved from `golang.org/x/exp` into the standard library, or between major versions. The good news: `go build` fails immediately and cheaply. Unlike npm or pip, **nothing gets installed and no code executes** during resolution — Go modules have no install scripts, which removes the entire slopsquatting execution vector. The bad news: someone can still register a plausible module path and wait. The check is the same as everywhere — before adding a dependency you did not choose deliberately, look at the repository: does it have history, other users, and issues from real people? ```bash go list -m -json github.com/some/module@latest # origin, version, checksum ``` ## The directives that disable the guarantees Three things in `go.mod` and the environment quietly turn off the protections above. All three have legitimate uses; all three deserve a comment. ### `replace` ```go replace github.com/upstream/lib => ../local-fork replace github.com/upstream/lib => github.com/ourfork/lib v1.2.3 ``` A local `replace` pointing outside the repository makes your build non-reproducible for anyone else. A `replace` to a fork is fine and should say why, with a link to the upstream issue and a plan to remove it. **Catch it with:** `grep -n '^replace' go.mod` in review. Note that `replace` directives in a dependency's `go.mod` are ignored — they only apply in the main module — which surprises people. ### `GOFLAGS=-mod=mod` and `GONOSUMDB`/`GOPRIVATE`/`GONOSUMCHECK` ```bash GOPRIVATE=github.com/yourorg/* # skips proxy AND checksum db for these paths GONOSUMDB=* # skips checksum verification. never do this. ``` `GOPRIVATE` is correct and necessary for internal modules. A wildcard that disables checksum verification broadly is not — and it is a common "fix" for a confusing build error. ### `// indirect` accumulation `go mod tidy` keeps this honest. A `go.mod` that has not been tidied accumulates requirements nothing uses, which inflates your vulnerability surface for no benefit. ```bash go mod tidy && git diff --exit-code go.mod go.sum # in CI: fails if not tidy ``` ## Reduce the surface Go's standard library is large, and generated Go reaches for dependencies it does not need — partly because a lot of pre-2021 Go in the training data predates the stdlib additions. | Generated reaches for | Often unnecessary because | |---|---| | `logrus`, `zap` | `log/slog` is stdlib since 1.21 | | `github.com/pkg/errors` | `fmt.Errorf` with `%w`, `errors.Is/As` | | `gorilla/mux`, `chi` for simple routing | `net/http` has method and wildcard patterns since 1.22 | | a `min`/`max`/`contains` helper | builtins and `slices` | | `github.com/google/uuid` for a random id | `crypto/rand` plus encoding, if you do not need UUID format | | `testify` | the stdlib `testing` package plus `cmp.Diff` | | `godotenv` | read the file, or use your platform's config | `testify` is worth singling out because it is nearly universal and genuinely optional: table-driven tests with `cmp.Diff` for comparison cover almost everything, with one fewer dependency and no assertion DSL to learn. Put the rule in [your `AGENTS.md`](/ai/agents-md/): *new dependencies need a sentence justifying them; prefer the standard library.* ## Read the go.sum diff ```bash go get github.com/some/lib git diff go.mod go.sum ``` A one-line `go.mod` change is often a forty-line `go.sum` change. That diff is the transitive dependencies you just accepted, and it is the only place you see them. A dependency that pulls in twenty modules for one function is a decision, not an accident. ```bash go mod graph | wc -l # how big is the graph? go mod why github.com/surprising/module # why is this here at all? ``` `go mod why` is the tool for the "where did *that* come from" moment, and it gives you the actual import chain. ## Keep it current ```bash govulncheck ./... # in CI, on every build go list -m -u all # what has newer versions go get -u ./... && go mod tidy && go test ./... ``` `govulncheck` in CI is the highest-value item here. Because it does reachability analysis, a failure means something real, so people fix it rather than muting it — which is the failure mode of noisier scanners in other ecosystems. Enable Dependabot or Renovate as well. Not because each update matters individually, but because a repository where updates arrive continuously is one where a security update can be merged in an afternoon rather than being a project. ## Vendoring ```bash go mod vendor # commits the dependency source into vendor/ ``` Worth it when you need builds that work with no network, or an auditable snapshot of exactly what ships. The cost is a large diff on every update and a `vendor/` directory people scroll past in review. For most projects, `go.sum` plus a module proxy gives you the same guarantees with less noise. :::verdict The whole policy 1. `go mod tidy` clean, enforced in CI. 2. `govulncheck` in CI. Act on it; it is low-noise by design. 3. Read the `go.sum` diff when adding anything. 4. Every `replace` has a comment saying why and when it goes. 5. Never disable checksum verification to make an error go away. ::: ## Common questions ### Can a Go module run code at install time? No. There is no install or post-install hook — `go get` downloads and verifies source, and nothing executes until you build and run. That removes the single most dangerous property of npm and PyPI installs, and it is why package-install permission matters less in Go than elsewhere. Build-time code generation via `go:generate` is explicit and only runs when you ask. ### Should package installs still go behind a confirmation for my agent? Less critical than in other ecosystems, but still worth it — not for the execution risk, which is absent, but because it makes you read the module path before it enters `go.mod`. Two seconds, and it catches the wrong-vanity-path case. ### What does minimal version selection mean in practice? You get the lowest version that satisfies every requirement in the graph, so adding a dependency does not silently upgrade your others, and builds are reproducible without a lockfile ceremony. Upgrades are explicit acts via `go get`, which is the behaviour you want. ### Is a private module proxy worth running? For a team with internal modules, yes — it gives you availability independent of upstream, a cache, and a single place to enforce policy. For a solo project, `GOPRIVATE` pointed at your VCS is enough. ## Writing an AGENTS.md for Go Source: https://learn-go.org/ai/agents-md/ `AGENTS.md` is a Markdown file in your repository root that coding agents read before they start. Claude Code reads `CLAUDE.md`; Codex, Cursor, Aider and most newer tools read `AGENTS.md`. Same contents, so write one and symlink: ```bash ln -s AGENTS.md CLAUDE.md ``` The file is not documentation. It is **a standing correction list**: the things a competent Go developer would get wrong in their first week in *your* repo. ## Go's file should be the shortest one you write Every token here is prepended to every request for the whole session. It is a permanent tax on context, and — more importantly — a long list dilutes the rules you care about. Twelve rules get followed; sixty get sampled. Go has an unusual advantage: `gofmt`, `go vet`, `staticcheck` and the compiler already enforce most of what other languages need prose for. Do not write down what `golangci-lint` will tell the model in one second. :::verdict The target **Under 60 lines.** If it is longer, most of it is either already enforced by a tool or is documentation that belongs in `docs/` behind a one-line pointer. ::: ## What not to write Delete these on sight. Every one is already covered: | Do not write | Because | |---|---| | "Format your code" | `gofmt`. There is no choice to make. | | "Remove unused imports and variables" | Compile errors. It cannot ship. | | "Use camelCase, exported names are capitalised" | The language enforces visibility; `staticcheck` covers naming. | | "Handle errors" | Ignoring one requires writing `_`, which `errcheck` flags. | | "Add comments to exported functions" | `revive` / `staticcheck` rule. Configure it once. | | "Don't use deprecated stdlib" | `staticcheck` SA1019. | That last one is worth a caveat: `staticcheck` catches deprecations, but models still reach for `ioutil`, `interface{}` and hand-rolled `contains` because the training data is full of them. One line covering the whole class is worth it — see the template. ## What is worth writing **How to run things.** Unguessable and used every turn. **Concurrency policy.** The one area where the compiler stops helping and where [generated Go is genuinely weak](/review/failure-modes/). This is the highest-value section in a Go `AGENTS.md`. **Interface placement.** Consumer-side interfaces are idiomatic Go and models trained on a lot of Java-shaped Go will not do it by default. **Landmines.** What breaks if changed. Almost nobody writes these and they are worth more than everything else combined. ## The template ```markdown AGENTS.md Go 1.23. Layout: cmd/ (binaries), internal/ (everything real), pkg/ (only if genuinely importable by other repos). ## Commands - Everything: `make check` (gofmt, go vet, golangci-lint, go test -race) - Test: `go test -race -count=1 -timeout 60s ./...` - One test: `go test -run TestName ./internal/pkg -v` - Bench: `go test -bench=. -benchmem ./internal/pkg` `-race` is always on and `-count=1` disables caching. Do not remove either to make the suite faster. ## Errors - Wrap with context and %w: `fmt.Errorf("load user %s: %w", id, err)`. Never %v — it silently breaks errors.Is for every caller. - Compare with errors.Is / errors.As, never ==. - Handle once: add context and return. Do not log and return the same error. - No naked returns. No panic outside main() and package init. ## Concurrency - Every goroutine needs a guaranteed exit path. If it can block on a send, buffer the channel or give it a context. - Prefer errgroup.WithContext over WaitGroup + channels by hand. - context.Context is the first parameter of anything doing I/O, and is actually plumbed through — not accepted and dropped. - TestMain calls goleak.VerifyTestMain. - Never range a map to produce output. Sort the keys. ## Style the linter cannot enforce - Interfaces are declared by the CONSUMER, in the consumer's package, and are small. One or two methods. Return concrete types. - Table-driven tests with t.Run subtests. - Use the current stdlib: os.ReadFile not ioutil, any not interface{}, slices/maps packages, log/slog not logrus, math/rand/v2. - No dependency injection framework. Wire it explicitly in main(). ## Landmines - internal/scheduler is leader-elected. Changing tick timing needs an ops review. - internal/proto is generated. Edit the .proto and run `make proto`. - cmd/migrate: write migrations, never run them. A human runs migrations. ``` Fifty lines, and almost none of it is generic Go advice — it is facts about this repository plus the two areas (errors, concurrency) where tooling alone is not enough. :::tip The two-strike rule Do not add a rule speculatively. Wait until an agent has made the same mistake twice. It keeps the file short, keeps every line evidence-backed, and tells you which of your conventions are genuinely non-obvious. ::: ## Push the enforceable parts into config Anything you *can* enforce, enforce — a check that fails beats a sentence that competes for attention. ```yaml .golangci.yml linters: enable: - errcheck # ignored errors - errorlint # %w and errors.Is — catches the wrapping rule above - contextcheck # dropped contexts - govet # lostcancel, copylocks, printf - staticcheck # deprecations, and most of the rest - bodyclose - noctx - gosec ``` That config removes three lines from the file above and makes them non-negotiable instead of advisory. Same trade every time it is available. ## Nested files in a monorepo Most tools read the nearest `AGENTS.md` and merge upward. In a repo with several services, keep the root file to commands and layout and push specifics down: ```text AGENTS.md commands, layout, error and concurrency policy internal/billing/AGENTS.md money is int64 cents; the Stripe webhook landmine internal/ingest/AGENTS.md backpressure rules, the bounded worker pool ``` ## Is yours working? Run the same non-trivial task twice — once with the file, once with it renamed away. If the diffs differ meaningfully, it is earning its place. If they do not, you have written fifty lines of ballast. It is also worth deleting a third of it every few months and seeing whether anything gets worse. In Go, usually nothing does, because the toolchain was already covering it. ## Common questions ### Why is the Go file shorter than the Python one? Because more of the equivalent content is enforced mechanically. `gofmt` removes every style rule, the compiler removes every dead-code rule, and `errcheck` removes the error-handling rule. What is left is genuinely repo-specific — which is what the file should have been all along. ### Should I mention the Go 1.22 loop variable change? Only if you see it causing confusion. Models trained on older code sometimes emit the `x := x` shadowing workaround, which is now unnecessary but harmless. The one worth catching is generated code that *relies on the old sharing behaviour*, and that is a bug a reviewer should spot rather than a line in a file. ### Do I need a section on generics? A line, if your codebase uses them: "prefer concrete types; generics need a reason." Generated generic Go tends to be more elaborate than the problem requires, because there is comparatively little good generic Go in the training data. ## Loops Source: https://learn-go.org/loops/ ## Loops Go has only one looping construct, the **for** loop. The basic **for** loop has three components separated by semicolons: - the init statement: executed before the first iteration - the condition expression: evaluated before every iteration - the post statement: executed at the end of every iteration The init statement will often be a short variable declaration, and the variables declared there are visible only in the scope of the for statement. The loop will stop iterating once the boolean condition evaluates to false. **Note**: Unlike other languages like *C*, *Java*, or *JavaScript* there are no parentheses surrounding the three components of the for statement and the braces { } are always required ## Examples ```go package main import "fmt" func main() { for i := 0; i < 10; i++ { fmt.Println(i) } } ``` ## Security review checklist for AI-generated Go Source: https://learn-go.org/review/security/ Go is a good language for writing secure code and generated Go is usually reasonable — parameterised queries by default, no buffer overflows, sane crypto defaults. What survives review is narrow and specific: **choosing the wrong package from a pair that look interchangeable**, and the handful of things `gosec` cannot infer. ## Turn on the machine checks first ```bash go install golang.org/x/vuln/cmd/govulncheck@latest govulncheck ./... # CVEs, but only for code paths you actually call golangci-lint run # with gosec enabled ``` `govulncheck` is the standout tool here and has no equivalent in most ecosystems: it does reachability analysis, so it reports the vulnerable functions your code can actually reach rather than every CVE in your dependency tree. That means near-zero noise, which means people act on it. ```yaml .golangci.yml linters: enable: [gosec, bodyclose, noctx, contextcheck, errcheck, errorlint, sqlclosecheck] ``` ## The wrong-package mistakes ### 1. `text/template` where `html/template` was needed The most consequential single mistake in generated Go web code. ```go import "text/template" // no escaping whatsoever t := template.Must(template.New("p").Parse(`
{{.Comment}}
`)) t.Execute(w, data) // stored XSS ``` The two packages have **identical APIs**, so swapping the import compiles and passes every test. `html/template` is contextually aware — it escapes differently inside an attribute, a URL, a script block — and `text/template` does nothing at all. **Correct:** `import "html/template"` for anything that reaches a browser. **Catch it with:** `grep -rn '"text/template"' --include='*.go' .` in review, and a line in [your `AGENTS.md`](/ai/agents-md/). No linter reliably distinguishes intent here. ### 2. `math/rand` where `crypto/rand` was needed ```go token := fmt.Sprintf("%x", rand.Int63()) // predictable ``` Session tokens, password reset links, nonces, API keys, filenames in a shared directory. `math/rand/v2` is better distributed than the old package and is still not cryptographically secure. ```go b := make([]byte, 32) if _, err := crypto_rand.Read(b); err != nil { return fmt.Errorf("generate token: %w", err) } token := base64.RawURLEncoding.EncodeToString(b) ``` **Catch it with:** `gosec` G404. ### 3. `==` on secrets ```go if mac == expectedMAC { // early return leaks timing ``` `subtle.ConstantTimeCompare` for MACs, tokens and password hashes. **Catch it with:** `gosec` G201-family rules, and review. ## Injection ### 4. SQL built with fmt.Sprintf ```go q := fmt.Sprintf("SELECT * FROM users WHERE email = '%s'", email) db.Query(q) ``` Still generated when the surrounding code does not use an ORM or `sqlc`. Use placeholders — `db.Query("... WHERE email = $1", email)`. **Catch it with:** `gosec` G201/G202. Note that a query built for a dynamic `ORDER BY` cannot be parameterised. That case needs an allowlist: ```go var allowed = map[string]string{"name": "name", "created": "created_at"} col, ok := allowed[req.SortBy] if !ok { return ErrBadSort } ``` ### 5. Command execution ```go exec.Command("sh", "-c", "git log --author="+author) // injection exec.Command("git", "log", "--author="+author) // fine: no shell ``` Go's `exec.Command` takes an argument slice and does **not** invoke a shell, which makes it safe by default — until generated code reaches for `sh -c` to get pipes or globbing. **Catch it with:** `grep -rn '"sh", "-c"' --include='*.go'` and `gosec` G204. Also check `exec.LookPath` behaviour: a relative or attacker-influenced `PATH` means you may run a different binary than you intended. ## Web-layer ### 6. SSRF ```go func fetchAvatar(w http.ResponseWriter, r *http.Request) { url := r.URL.Query().Get("url") resp, _ := http.Get(url) // now fetches your metadata endpoint ``` Generated "import from URL", webhook and avatar features almost never validate. Allowlist the scheme and host, resolve the name and reject private ranges, and disable redirects: ```go client := &http.Client{ CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse // do not follow into the private range }, Timeout: 10 * time.Second, } ``` A redirect check matters: an attacker controls a public host that 302s to `169.254.169.254`. ### 7. Path traversal ```go http.ServeFile(w, r, filepath.Join(dir, r.URL.Path)) // ../../etc/passwd ``` Use `http.FileServer` with `http.Dir`, which handles this, or `os.Root` (Go 1.24+) for a directory you cannot escape. If you must join manually, `filepath.Clean` then verify the result is still inside the base with a separator-aware prefix check. ### 8. Missing timeouts ```go http.ListenAndServe(":8080", mux) // no timeouts at all ``` The zero-value `http.Server` has no read, write or idle timeout, so a slow client can hold a connection indefinitely. Generated servers use the one-liner constantly. ```go srv := &http.Server{ Addr: ":8080", Handler: mux, ReadHeaderTimeout: 5 * time.Second, // the Slowloris one ReadTimeout: 15 * time.Second, WriteTimeout: 30 * time.Second, IdleTimeout: 60 * time.Second, MaxHeaderBytes: 1 << 20, } ``` **Catch it with:** `gosec` G112 for `ReadHeaderTimeout` specifically. ### 9. Unbounded reads ```go body, _ := io.ReadAll(r.Body) // attacker decides your memory usage ``` `http.MaxBytesReader(w, r.Body, 1<<20)` first. Same for `json.Decoder` — and use `DisallowUnknownFields` to catch mass assignment through an API you did not intend to expose. ### 10. Decompression bombs `gzip.NewReader` followed by `io.ReadAll` will happily expand a small upload into all your memory. Wrap with `io.LimitReader` on the *decompressed* side. ## Types and numbers ### 11. Integer conversion truncating ```go var n int64 = userSuppliedValue buf := make([]byte, int32(n)) // silently truncates or goes negative ``` Go 1.24's vet includes checks for some of these, and `gosec` G115 flags integer overflow conversions. Worth enabling — it is a real source of allocation bugs at boundaries. ### 12. Errors ignored on security-relevant calls ```go _ = json.Unmarshal(data, &claims) // parse failed; claims is zero-valued if claims.Role == "admin" { } // zero value is "", so this is fine — this time ``` The pattern is dangerous whenever the zero value happens to be permissive. `errcheck` catches the discard; review catches whether the zero value is safe. ## Authorisation The most common real vulnerability in any web codebase, and no linter finds it: ```go func (h *Handler) GetInvoice(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") inv, err := h.store.Invoice(r.Context(), id) // any authenticated user, any invoice ``` The ownership check belongs beside the query: ```go inv, err := h.store.InvoiceForUser(r.Context(), id, user.ID) if errors.Is(err, sql.ErrNoRows) { http.NotFound(w, r) // 404, not 403 — do not confirm it exists return } ``` Write the test once per resource type: ```go func TestCannotReadAnotherUsersInvoice(t *testing.T) { rr := doRequest(t, "GET", "/invoices/"+bobInvoiceID, aliceToken) if rr.Code != http.StatusNotFound { t.Errorf("status = %d, want 404", rr.Code) } } ``` ## TLS and crypto Go's defaults are good. What generated code gets wrong is disabling them: ```go tls.Config{InsecureSkipVerify: true} // never in production tls.Config{MinVersion: tls.VersionTLS10} // set 1.2 or 1.3 ``` `InsecureSkipVerify` appears in generated code as a way past a certificate error during development and then survives into main. **Catch it with:** `gosec` G402, and a grep in review. ## The review, as commands ```bash govulncheck ./... golangci-lint run grep -rn '"text/template"' --include='*.go' . # XSS grep -rn 'InsecureSkipVerify' --include='*.go' . grep -rn '"sh", "-c"' --include='*.go' . grep -rn 'math/rand' --include='*.go' . | grep -iE 'token|secret|key|nonce|session' grep -rn 'ListenAndServe' --include='*.go' . # missing timeouts ``` Under a minute, and it covers the automatable half. The half worth your attention is authorisation and SSRF, because both depend on knowing what your system is supposed to allow. :::verdict The Go-specific thing to remember Two pairs of standard library packages look identical and are not: `text/template` vs `html/template`, and `math/rand` vs `crypto/rand`. Picking the wrong one compiles, passes tests, and is a vulnerability. Grep for both on every review. ::: ## Common questions ### Is `govulncheck` better than a generic scanner? For Go, substantially — it uses call-graph reachability, so it reports vulnerabilities your code can actually reach rather than every CVE in the module graph. That means far less noise, which is the difference between a tool people act on and one they mute. ### Does the standard library's `net/http` need a framework to be secure? No. It needs timeouts set explicitly, body size limits, and `html/template` for rendering. A framework may give you those by default, which is a real convenience, but nothing about `net/http` is insecure once configured. ### Why does generated Go use `InsecureSkipVerify`? Because it is the answer to "how do I get past this certificate error", and a great deal of example code contains it. Treat any occurrence as a defect and require a comment explaining the specific test scenario if it must exist at all. ### Should I use `os.Root` for file serving? If you are on Go 1.24 or later and serving files from a directory, yes — it gives you a handle that cannot be escaped by traversal or symlinks, which is a stronger guarantee than any amount of path cleaning. ## Testing Go that calls a language model Source: https://learn-go.org/ai/evals/ Go turns up in this domain in a particular role: the gateway, the proxy, the worker, the orchestrator. The service that fans out to three providers, enforces a budget, retries, streams back, and records what happened. That shape is good news for testing, because the interesting behaviour is concurrency, cancellation and failure handling — which is exactly what Go's standard library is best at exercising. ## The interface is the whole design Define it in the package that *uses* it, keep it small, and everything above becomes testable with no network. ```go internal/llm/llm.go package llm type Request struct { System string Messages []Message MaxTokens int } type Response struct { Text string Usage Usage } // Client is what the rest of the application depends on. type Client interface { Complete(ctx context.Context, req Request) (Response, error) Stream(ctx context.Context, req Request) (iter.Seq2[string, error], error) } ``` Two methods. A real implementation, a fake, and — as it turns out — you rarely need the fake, because `httptest` gives you something better. ## Prefer httptest to a hand-written fake A fake `Client` tests your code against your assumptions. An `httptest.Server` tests it against real HTTP semantics: status codes, headers, chunked transfer, connections that close mid-body, contexts that cancel. ```go internal/llm/client_test.go func TestRetriesOn429ThenSucceeds(t *testing.T) { var calls atomic.Int32 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if calls.Add(1) == 1 { w.Header().Set("Retry-After", "0") w.WriteHeader(http.StatusTooManyRequests) return } _ = json.NewEncoder(w).Encode(apiResponse{Text: `{"category":"billing"}`}) })) defer srv.Close() c := New(srv.URL, WithMaxRetries(3)) got, err := c.Complete(t.Context(), Request{System: "s"}) if err != nil { t.Fatalf("Complete() error = %v", err) } if calls.Load() != 2 { t.Errorf("calls = %d, want 2", calls.Load()) } if got.Text == "" { t.Error("empty text") } } ``` The cases worth writing, all of which are real production failures and none of which need a model: | Test | What it catches | |---|---| | 429 with `Retry-After` | retry logic, and that you honour the header | | 500 on every attempt | that you give up rather than retry forever | | Response body closed mid-stream | partial-write handling | | Server sleeps past the deadline | that your context timeout actually fires | | Malformed JSON | parser never panics | | Empty 200 | the "provider returned nothing" path | :::warn The one Go-specific bug worth naming A handler that takes a `context.Context` and does not pass it to the outbound request. Cancellation silently stops working: the user's request is abandoned, and your worker keeps streaming tokens you are billed for. `contextcheck` in `golangci-lint` catches most instances — see [the Go failure modes](/review/failure-modes/). ::: ## Cancellation is the test people skip ```go func TestClientDisconnectCancelsUpstream(t *testing.T) { released := make(chan struct{}) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { <-r.Context().Done() // upstream sees the cancellation close(released) })) defer srv.Close() ctx, cancel := context.WithCancel(t.Context()) go func() { time.Sleep(20 * time.Millisecond); cancel() }() c := New(srv.URL) if _, err := c.Complete(ctx, Request{}); !errors.Is(err, context.Canceled) { t.Fatalf("err = %v, want context.Canceled", err) } select { case <-released: case <-time.After(time.Second): t.Fatal("upstream request was never cancelled — you are still paying for this") } } ``` That test pays for itself the first time someone closes a browser tab. And because this is Go, put the leak check in place once and forget it: ```go func TestMain(m *testing.M) { goleak.VerifyTestMain(m) } ``` Streaming code with a `for range` over a channel is the most common source of goroutine leaks in this kind of service, and `goleak` turns each one into a test failure rather than a slow memory climb. ## Contract tests, table-driven Test what your code does with a response, not what the model says. ```go func TestParseCategory(t *testing.T) { tests := []struct { name string raw string want Category ok bool }{ {"plain", `{"category":"billing"}`, Billing, true}, {"fenced", "```json\n{\"category\":\"billing\"}\n```", Billing, true}, {"preamble", "Sure!\n{\"category\":\"billing\"}", Billing, true}, {"wrong case", `{"category":"Billing"}`, Billing, true}, {"unknown value", `{"category":"refunds_and_returns"}`, "", false}, {"misspelled key", `{"categorie":"billing"}`, "", false}, {"prose", "I think this is a billing issue.", "", false}, {"truncated", `{"category":"billing"`, "", false}, {"empty", ``, "", false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, ok := ParseCategory(tt.raw) if ok != tt.ok || got != tt.want { t.Errorf("ParseCategory() = %q, %v; want %q, %v", got, ok, tt.want, tt.ok) } }) } } ``` The contract is **never panic, never invent**. Returning a plausible wrong category is worse than returning `false`. ## Golden files instead of cassettes Go's idiom for recorded responses is a golden file plus an `-update` flag. Simpler than the equivalents elsewhere and it lives in the standard toolchain. ```go var update = flag.Bool("update", false, "rewrite golden files") func TestPromptRendering(t *testing.T) { got := BuildPrompt(fixtureQuery(), fixtureChunks()) golden := filepath.Join("testdata", "prompt.golden") if *update { if err := os.WriteFile(golden, []byte(got), 0o644); err != nil { t.Fatal(err) } } want, err := os.ReadFile(golden) if err != nil { t.Fatal(err) } if diff := cmp.Diff(string(want), got); diff != "" { t.Errorf("prompt mismatch (-want +got):\n%s", diff) } } ``` ```bash go test ./... -update # re-record, then read the diff before committing ``` Reading that diff is the point. A prompt change you did not intend shows up here before it shows up in your eval scores. Store recorded API responses in `testdata/` the same way and serve them from your `httptest` handler. Scrub credentials before committing, and re-record on a schedule — a golden file from eighteen months ago is testing a model that no longer exists. ## Concurrency: the part Go actually adds If your service fans out, test the fan-out. This is where a Go LLM gateway earns its keep and where it breaks. ```go func TestFanOutRespectsConcurrencyLimit(t *testing.T) { var inFlight, peak atomic.Int32 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { n := inFlight.Add(1) for { p := peak.Load() if n <= p || peak.CompareAndSwap(p, n) { break } } time.Sleep(10 * time.Millisecond) inFlight.Add(-1) _, _ = w.Write([]byte(`{"text":"ok"}`)) })) defer srv.Close() if err := ClassifyAll(t.Context(), New(srv.URL), makeInputs(50), WithLimit(5)); err != nil { t.Fatal(err) } if got := peak.Load(); got > 5 { t.Errorf("peak concurrency = %d, want <= 5", got) } } ``` Run it with `-race`, always. Unbounded fan-out at a rate-limited provider is the most common way one of these services falls over, and it is a five-line `errgroup.SetLimit` fix once you know. ## Evals Nightly, behind a build tag so they never run in the normal suite. ```go //go:build eval package eval func TestClassificationAccuracy(t *testing.T) { cases := loadDataset(t, "testdata/dataset.jsonl") client := llm.NewFromEnv() var hits int for _, c := range cases { got, err := Classify(t.Context(), client, c.Input) if err != nil { t.Errorf("%q: %v", c.Input, err) continue } if got == c.Expect { hits++ continue } t.Logf("MISS %q: got %s, want %s", c.Input, got, c.Expect) } acc := float64(hits) / float64(len(cases)) t.Logf("accuracy %.1f%% on %d cases", acc*100, len(cases)) if acc < 0.90 { t.Errorf("accuracy %.1f%% below threshold 90%%", acc*100) } } ``` ```bash go test -tags eval ./eval/... # nightly only ``` `t.Logf` for the misses rather than `t.Errorf` — the individual failures are information, the aggregate is the gate. Read the misses; that list is where the next prompt change comes from. :::note The dataset is the work Fifty real, awkward examples beat five hundred synthetic ones. Take them from production logs. Every time something goes wrong in production, the input becomes a case in `testdata/`. That is the flywheel; the harness above is forty lines. ::: ## The whole loop ```makefile test: # every edit, no network, no tokens go test -race -count=1 -timeout 60s ./... eval: # nightly go test -tags eval -count=1 -timeout 20m ./eval/... ``` Fast, honest, free, and it exercises the parts of an LLM service that actually break. ## Common questions ### Should I use an SDK or call the HTTP API directly? For a gateway or proxy — the shape Go usually takes here — the raw API is often simpler. You are already handling streaming, retries and budgets yourself, and an SDK mostly adds a layer between you and the behaviour you are trying to control. For an application that makes occasional calls, use the SDK. ### Why `httptest` rather than a mock client? A mock tests your code against your assumptions about HTTP. `httptest` tests it against real HTTP: connection resets, chunked encoding, contexts that cancel mid-body. Those are where the bugs are, and they are the cases a hand-written fake never simulates. ### How do I test streaming? Have your `httptest` handler write chunks with an explicit `Flush()` between them, and vary the chunk boundaries — including one that splits a multi-byte UTF-8 character. That, plus a handler that closes the connection halfway, covers most streaming bugs. ### How do I stop the eval run costing a fortune? Build tags keep it out of the normal suite, which is most of it. Beyond that: bound the fan-out with `errgroup.SetLimit`, use the batch API where latency does not matter, and enforce a budget in the context — see [tracking and cutting token costs in Go](/ai/tokenomics/). ### Do golden files not just get blindly updated? They do if nobody reads the diff, which is why the discipline is to run `-update` and then read `git diff testdata/` before committing. That habit is the entire value; without it a golden file is a test that always passes. ## Functions Source: https://learn-go.org/functions/ ```go func add(a int, b int) int { return a + b } func addSub(a, b int) (int, int) { // same type? declare it once return a + b, a - b } ``` ## Multiple return values This is the feature that defines Go's style. Almost every function that can fail returns two values, and the second is an `error`. ```go func divide(a, b float64) (float64, error) { if b == 0 { return 0, fmt.Errorf("divide %v by zero", a) } return a / b, nil } result, err := divide(10, 2) if err != nil { return fmt.Errorf("compute ratio: %w", err) } ``` Three conventions worth learning immediately: - The error is always the **last** return value. - On error, other return values are their zero value and must not be used. - `%w` in `fmt.Errorf` wraps the error so `errors.Is` and `errors.As` can find it later. `%v` does not — that difference is invisible in the message and breaks callers. ## Named return values ```go func split(sum int) (x, y int) { x = sum * 4 / 9 y = sum - x return // "naked" return — returns x and y } ``` Useful for documenting what each value means, and useful with `defer` for modifying a result on the way out. Avoid naked returns in anything longer than a few lines: the reader has to scroll to find what is actually returned. ## Variadic functions ```go func sum(nums ...int) int { total := 0 for _, n := range nums { total += n } return total } sum(1, 2, 3) sum(values...) // spread a slice ``` ## Functions are values ```go func apply(nums []int, f func(int) int) []int { out := make([]int, len(nums)) for i, n := range nums { out[i] = f(n) } return out } doubled := apply([]int{1, 2, 3}, func(n int) int { return n * 2 }) ``` ## Closures ```go func counter() func() int { count := 0 return func() int { count++ return count } } next := counter() next() // 1 next() // 2 ``` Each call to `counter` gets its own `count`. ## defer `defer` schedules a call to run when the surrounding function returns — including when it returns because of a panic. ```go func readConfig(path string) ([]byte, error) { f, err := os.Open(path) if err != nil { return nil, fmt.Errorf("open %s: %w", path, err) } defer f.Close() // runs no matter which return we take return io.ReadAll(f) } ``` Deferred calls run last-in-first-out, and their arguments are evaluated immediately — at the `defer` line, not when it runs. That trips people up exactly once. :::warn Closing a writable file `defer f.Close()` silently discards the close error. For a file you wrote to, that error is how you find out the write failed. Capture it with a named return: ```go func write(path string, data []byte) (err error) { f, err := os.Create(path) if err != nil { return err } defer func() { if cerr := f.Close(); cerr != nil && err == nil { err = cerr } }() _, err = f.Write(data) return err } ``` ::: ## Common questions ### Why does Go not have default parameter values? Deliberate simplicity: there is one calling convention and no overload resolution to reason about. The idiomatic replacements are a config struct, or functional options (`WithTimeout(5*time.Second)`) when a package needs many optional settings. ### When should I use named return values? When the names document something the types do not — `(width, height int)` — or when you need `defer` to modify the result, as in the close example above. Otherwise prefer explicit returns. ### Is `defer` expensive? Not meaningfully since Go 1.14; it is close to free in the common case. Use it. The one place to think about it is a `defer` inside a hot loop, where it accumulates until the function returns rather than the loop iteration. ## The performance traps in generated Go Source: https://learn-go.org/review/performance/ Generated Go is usually clear and usually allocates more than it needs to. The good news is that Go has the best built-in performance tooling of any language here: benchmarks, allocation counts and profiling all ship with the compiler. :::note Measure first, and Go makes it easy ```bash go test -bench=. -benchmem ./... # ns/op AND allocations per op go test -bench=BenchmarkX -cpuprofile=cpu.out -memprofile=mem.out ./pkg go tool pprof -http=:8080 cpu.out ``` `-benchmem` is the flag to internalise. `allocs/op` is a more stable signal than `ns/op` and it points directly at most of this page. ::: ## Allocation ### 1. Slices and maps without capacity The most common allocation waste in generated Go. ```go var out []string // nil; grows by repeated reallocation for _, u := range users { out = append(out, u.Name) } ``` Each growth allocates a new backing array and copies. When you know the size, say so: ```go out := make([]string, 0, len(users)) // one allocation for _, u := range users { out = append(out, u.Name) } ``` Same for maps: `make(map[string]int, len(items))`. This is the single easiest win in the language and generated code omits it nearly every time. ### 2. String concatenation in a loop ```go var s string for _, part := range parts { s += part // a new string every iteration: quadratic } ``` Strings are immutable, so `+=` allocates and copies the whole accumulated string each time. ```go var b strings.Builder b.Grow(estimatedSize) // optional but free for _, part := range parts { b.WriteString(part) } s := b.String() ``` Or `strings.Join(parts, "")` when you already have the slice. **Look for:** `+=` on a string inside any loop. ### 3. Unnecessary conversions between string and []byte ```go for _, line := range lines { if bytes.Contains([]byte(line), needle) { // allocates a copy per iteration ``` `[]byte(s)` and `string(b)` both copy. Pick one representation and stay in it. The compiler elides the conversion in a few specific cases (map lookups, `range`, comparisons) but not in general. ### 4. Interface boxing in hot paths Passing a small value as `any` allocates, because the value must escape to the heap to be stored in the interface. `fmt.Sprintf` in a hot loop is the usual culprit — it boxes every argument and does reflection. ```go // hot path: use strconv, not fmt s := strconv.Itoa(n) // no allocation for small ints s := fmt.Sprintf("%d", n) // boxes, reflects, allocates ``` ### 5. Escape analysis surprises ```bash go build -gcflags='-m' ./... 2>&1 | grep 'escapes to heap' ``` Returning a pointer to a local, capturing a variable in a closure that outlives the function, or storing something in an interface all force a heap allocation. The compiler will tell you exactly which and why. Worth running once on a hot package — it is often surprising. ## Concurrency ### 6. Unbounded goroutines ```go for _, url := range urls { // 50,000 urls, 50,000 goroutines go fetch(url) } ``` Goroutines are cheap, not free, and the resource they exhaust is usually the thing at the other end — file descriptors, connections, a rate-limited API. Generated fan-out code omits the limit almost always. ```go g, ctx := errgroup.WithContext(ctx) g.SetLimit(runtime.GOMAXPROCS(0) * 4) // or a number matched to the downstream for _, url := range urls { g.Go(func() error { return fetch(ctx, url) }) } err := g.Wait() ``` One line — `SetLimit` — and it is the difference between a working batch job and a 429 storm. ### 7. Mutex contention where an atomic would do `sync.Mutex` around a counter increment. `atomic.Int64` is dramatically cheaper under contention and reads more clearly. For read-heavy shared state, `sync.RWMutex` or a copy-on-write pointer swap via `atomic.Pointer` both beat a plain mutex — but measure, because `RWMutex` has more overhead than `Mutex` at low contention. ### 8. Channels used as a queue for cheap work A channel per item adds synchronisation cost that can exceed the work being done. For fine-grained parallel work over a slice, partitioning the slice across N goroutines with no channel at all is usually much faster. ### 9. GOMAXPROCS in a container Go reads the number of host CPUs, not your container's CPU limit. On a 64-core node with a 2-CPU limit, the runtime creates 64 threads and then gets throttled, which shows up as terrible tail latency for no obvious reason. Go 1.25 made the runtime container-aware, but if you are on anything older, set it explicitly or use `automaxprocs`. This is one of the highest-impact production Go issues and it has nothing to do with your code. ## I/O and data ### 10. N+1 queries ```go for _, o := range orders { items, _ := db.ItemsForOrder(ctx, o.ID) // one round trip per order o.Items = items } ``` Correct, passes tests with three orders, 101 round trips in production. Fetch with `WHERE order_id = ANY($1)` and group in memory. **Catch it with:** a test asserting query count. It is the only reliable defence, and it is the highest-value performance test in a service. ### 11. Unbuffered file and network I/O `os.File.Read` in a small loop, or writing per line without `bufio.Writer`. Generated I/O code is correct and unbuffered. ```go w := bufio.NewWriterSize(f, 64<<10) defer w.Flush() // and check the error — see below ``` ### 12. `defer` inside a loop ```go for _, path := range paths { f, _ := os.Open(path) defer f.Close() // accumulates until the FUNCTION returns } ``` Not a performance problem so much as a descriptor leak that presents as one. Move the body into its own function, or close explicitly. ### 13. JSON in a hot path `encoding/json` uses reflection. For a hot serialisation path it is often the bottleneck, and the fixes in order of effort are: reuse `json.Decoder` on a stream rather than `Unmarshal` on a full buffer, avoid `map[string]any` in favour of structs, and only then consider a code-generating alternative. ## Benchmarks worth writing ```go func BenchmarkFormatRows(b *testing.B) { rows := makeRows(1000) b.ReportAllocs() b.ResetTimer() for b.Loop() { // Go 1.24+; prevents the compiler eliding the work _ = FormatRows(rows) } } ``` ```bash go test -bench=. -benchmem -count=10 ./pkg > new.txt benchstat old.txt new.txt # is the difference real, or noise? ``` `benchstat` is the part people skip. A single benchmark run is noise; ten runs plus `benchstat` tells you whether a change is statistically real, which stops a lot of pointless optimisation. :::verdict The reviewer's shortcut Scan the diff for loops and for `go` statements. For each loop: **does it append without preallocating, concatenate a string, or make a database call?** For each `go` statement: **is the concurrency bounded?** Those two questions catch items 1, 2, 6 and 10 — most of the real cost on this page. ::: ## Common questions ### Should I preallocate everywhere? Wherever you know the size, yes — `make([]T, 0, n)` is free to write and removes repeated growth. Where you genuinely do not know, `append` on a nil slice is fine and its growth strategy is reasonable. Do not invent a capacity you cannot justify. ### Is `sync.Pool` worth using? Only for genuinely hot paths with large, reusable buffers, and only after profiling shows allocation is the bottleneck. It adds complexity and correctness risk (objects must be reset), and misused it can be slower than allocating. `bytes.Buffer` reuse in an HTTP handler is the classic legitimate case. ### Do goroutines have a meaningful cost? A few kilobytes of stack each, so a hundred thousand is fine in itself. What is not fine is a hundred thousand simultaneous requests to something with a connection limit. Bound the concurrency to match the constrained resource, not the goroutine cost. ### Why is my service slow in Kubernetes but fast locally? Check `GOMAXPROCS` against your CPU limit first. A runtime that thinks it has 64 cores inside a 2-CPU container creates far too many OS threads and gets CFS-throttled, which looks like mysterious tail latency and is not visible in any profile of your own code. ## Tracking and cutting token costs in Go Source: https://learn-go.org/ai/tokenomics/ The economics are language-independent — [what tokens cost and where the money goes](https://codelearningdojo.com/token-economics/) is the model. This page is the Go implementation, and Go usually occupies a specific position in these systems: the gateway, the proxy, the worker pool. The service that sits between many callers and a metered API. That position is where cost control belongs, and Go is unusually well suited to it: contexts propagate cancellation, `sync/atomic` makes counters cheap, and `errgroup` bounds fan-out in one line. ## Carry the budget in the context The idiomatic Go answer to "every call site needs this and I do not want it in every signature". ```go internal/budget/budget.go package budget import ( "context" "errors" "sync/atomic" ) var ErrExceeded = errors.New("budget exceeded") // Budget is safe for concurrent use. type Budget struct { capMicroUSD int64 maxTurns int64 spentMicros atomic.Int64 turns atomic.Int64 } func New(capMicroUSD int64, maxTurns int) *Budget { return &Budget{capMicroUSD: capMicroUSD, maxTurns: int64(maxTurns)} } // Reserve is called before a request leaves. It is deliberately conservative: // it charges the estimate up front and refunds the difference afterwards, so // concurrent callers cannot all pass the check and then collectively overspend. func (b *Budget) Reserve(estimateMicros int64) error { if b.turns.Add(1) > b.maxTurns { return errors.Join(ErrExceeded, errors.New("turn limit")) } if b.spentMicros.Add(estimateMicros) > b.capMicroUSD { b.spentMicros.Add(-estimateMicros) return ErrExceeded } return nil } func (b *Budget) Settle(estimateMicros, actualMicros int64) { b.spentMicros.Add(actualMicros - estimateMicros) } func (b *Budget) Spent() int64 { return b.spentMicros.Load() } type ctxKey struct{} func With(ctx context.Context, b *Budget) context.Context { return context.WithValue(ctx, ctxKey{}, b) } func From(ctx context.Context) (*Budget, bool) { b, ok := ctx.Value(ctxKey{}).(*Budget) return b, ok } ``` The reserve-then-settle pattern is the part worth copying. A naive "check, then call, then add" lets ten concurrent goroutines all pass the check against the same stale total and blow the cap tenfold. Reserving up front and refunding the difference is correct under concurrency, which in a Go gateway is the only case that matters. :::warn Integer micro-dollars, not float `int64` micro-dollars are exact and atomic. A `float64` accumulator loses fractions across millions of rows, and there is no atomic float. This is the same reasoning as [money in any language](/review/failure-modes/), with the extra constraint that you need `atomic` here. ::: ## Cancellation is a cost control If the caller goes away and you keep streaming from the provider, you keep paying. In Go this is nearly free to get right, and it is the most common thing generated gateway code omits. ```go internal/handler/chat.go func (h *Handler) Chat(w http.ResponseWriter, r *http.Request) { ctx := r.Context() // cancelled when the client disconnects b := budget.New(500_000, 12) ctx = budget.With(ctx, b) flusher, _ := w.(http.Flusher) var usage llm.Usage stream, err := h.llm.Stream(ctx, req) // ctx MUST reach the outbound request if err != nil { http.Error(w, err.Error(), http.StatusBadGateway) return } defer func() { h.record(ctx, "chat", usage, ctx.Err() != nil) }() for chunk, err := range stream { if err != nil { return } if _, werr := io.WriteString(w, chunk.Text); werr != nil { return // client gone; ctx cancels upstream } usage = chunk.Usage if flusher != nil { flusher.Flush() } } } ``` The whole fix is that `r.Context()` reaches the outbound HTTP request. A handler that accepts a context and then builds its upstream call with `context.Background()` has silently disabled cancellation — `contextcheck` in `golangci-lint` catches most instances, and it is worth turning on for exactly this reason. See [the Go failure modes](/review/failure-modes/). Record on cancellation too. You still paid for input tokens and whatever output was generated before the abort, so dropping those records understates spend. ## Bound the fan-out Unbounded `go` statements against a rate-limited API is the classic way a Go gateway turns a batch job into a 429 storm plus a retry bill. ```go func ClassifyAll(ctx context.Context, c llm.Client, inputs []string, limit int) ([]Category, error) { g, ctx := errgroup.WithContext(ctx) g.SetLimit(limit) // the whole fix out := make([]Category, len(inputs)) for i, in := range inputs { g.Go(func() error { b, _ := budget.From(ctx) est := int64(len(in)/4) * 3 // rough micros; conservative if b != nil { if err := b.Reserve(est); err != nil { return fmt.Errorf("classify %d: %w", i, err) } } res, usage, err := c.Complete(ctx, promptFor(in)) if b != nil { b.Settle(est, cost(usage)) } if err != nil { return fmt.Errorf("classify %d: %w", i, err) } out[i] = res return nil }) } return out, g.Wait() } ``` `errgroup.WithContext` also cancels every sibling on the first failure — so a budget exhaustion stops the remaining work rather than letting it run to completion and overspend. ## Make the cache hit Same rule as everywhere: stable prefix first, volatile last. The Go-specific trap is **map iteration order**, which is deliberately randomised. ```go // BAD: tool definitions from a map iterate in a different order every run, // so the prefix differs every call and the cache never hits. for name, tool := range h.tools { defs = append(defs, tool.Definition(name)) } // GOOD: deterministic order for _, name := range slices.Sorted(maps.Keys(h.tools)) { defs = append(defs, h.tools[name].Definition(name)) } ``` This one is genuinely easy to miss because nothing fails — the responses are correct, the tests pass, and the only symptom is a cache hit rate near zero. It is the Go flavour of a mistake every language has a version of. ```go func TestPromptPrefixIsStable(t *testing.T) { a := BuildMessages("q1", []string{"doc-b", "doc-a"}) b := BuildMessages("q2", []string{"doc-a", "doc-b"}) if diff := cmp.Diff(a[:len(a)-1], b[:len(b)-1]); diff != "" { t.Errorf("prefix is not stable, cache will miss (-a +b):\n%s", diff) } } ``` ## Retries, bounded ```go func withRetry(ctx context.Context, attempts int, fn func() error) error { var err error for n := 0; n < attempts; n++ { if err = fn(); err == nil { return nil } var apiErr *llm.APIError if !errors.As(err, &apiErr) || !apiErr.Retryable() { return err // never retry a 400 } delay := time.Duration(1< int s := strconv.Itoa(42) // int -> string f, err := strconv.ParseFloat("3.14", 64) b, err := strconv.ParseBool("true") s := strconv.FormatInt(255, 16) // "ff" ``` :::warn `string(65)` does not do what you expect ```go string(65) // "A" — interprets 65 as a code point strconv.Itoa(65) // "65" — what you almost certainly wanted ``` `go vet` flags the conversion. Use `strconv` for numbers and `fmt.Sprintf` for anything composite. ::: ## Formatting ```go fmt.Sprintf("%s is %d years old", name, age) fmt.Sprintf("%.2f", 3.14159) // "3.14" fmt.Sprintf("%v", someStruct) // default representation fmt.Sprintf("%+v", someStruct) // with field names fmt.Sprintf("%#v", someStruct) // Go syntax fmt.Sprintf("%T", someValue) // the type fmt.Sprintf("%q", "hi") // `"hi"` — quoted ``` `%+v` is the one to remember for debugging structs, and `%T` for "what actually is this". ## Exercise ```go package main import "fmt" func main() { phrase := "Go is 简单" // Print, one per line: // the byte length // the rune count // each rune with its byte index, using range // the phrase reversed BY RUNE (not by byte) fmt.Println(phrase) } ``` ## Common questions ### Why is `len()` giving me the wrong length? It is giving you the byte length, which differs from the character count for any non-ASCII text. Use `utf8.RuneCountInString` for characters, and `range` to iterate them safely. ### When should I use `[]byte` instead of `string`? When you are building or mutating data, doing I/O, or working with a package that expects bytes. Strings are immutable so every modification copies; `[]byte` avoids that. Converting between them copies, so pick one representation and stay in it within a hot path. ### Is `strings.Builder` worth it for a few concatenations? No — for two or three, `+` is clearer and the difference is irrelevant. It matters inside loops, where `+=` is quadratic because each step copies everything accumulated so far. ## Packages and Modules Source: https://learn-go.org/packages-and-modules/ Every Go file starts by declaring its package: ```go package main ``` A package is a directory. Every `.go` file in that directory must declare the same package name, and together they form one unit — files in the same package can use each other's identifiers with no import. `package main` is special: it produces an executable, and needs a `func main()`. ## Exported means capitalised Go's only access control is the case of the first letter: ```go package pricing const TaxRate = 0.2 // exported — usable as pricing.TaxRate const baseMargin = 0.15 // unexported — package-private func Total(items []Item) int { } // exported func applyMargin(n int) int { } // unexported type Item struct { SKU string // exported field costCents int // unexported field } ``` That is the whole mechanism. No `public`, no `private`. It applies to constants, variables, functions, types, methods and struct fields alike. The consequence worth knowing early: **an unexported field is invisible to `encoding/json`**, so a struct field you forgot to capitalise silently disappears from your API responses. ## Modules A module is a collection of packages versioned together, defined by `go.mod` at its root: ```bash go mod init github.com/you/myapp ``` ```go go.mod module github.com/you/myapp go 1.23 require ( github.com/google/uuid v1.6.0 golang.org/x/sync v0.8.0 ) ``` The module path is the import prefix for everything inside it. A package in `internal/pricing/` is imported as `github.com/you/myapp/internal/pricing`. ```bash go get github.com/google/uuid # add a dependency go mod tidy # add what is used, remove what is not go mod why github.com/x/y # why is this in my graph? ``` `go.sum` records a cryptographic hash of every module version, verified on every build. Commit both files. ## Imports ```go import ( "fmt" // standard library "net/http" "github.com/google/uuid" // third party "github.com/you/myapp/internal/pricing" // your own ) ``` `gofmt` groups and sorts these. Two forms worth knowing: ```go import mrand "math/rand" // alias, to disambiguate import _ "github.com/lib/pq" // blank — for side effects only (driver registration) ``` An unused import is a **compile error**, not a warning. That is a small thing that keeps generated Go tidy in a way other languages are not. ## The internal directory A package under a directory named `internal` can only be imported by code within the same module subtree: ```text github.com/you/myapp/ internal/pricing/ <- importable only inside myapp pkg/client/ <- importable by anyone ``` This is enforced by the compiler and is the right default: **put everything in `internal/` unless you intend other projects to import it.** Once something is publicly importable you own its API. ## A standard layout ```text myapp/ go.mod go.sum cmd/ server/main.go # one directory per binary worker/main.go internal/ pricing/ # domain logic pricing.go pricing_test.go storage/ # database access httpapi/ # handlers pkg/ # only if genuinely reusable by others ``` `cmd/` holds thin `main` packages that wire things together; the real code lives in `internal/`. For a small tool, a single `main.go` at the root is perfectly fine — do not build this structure before you need it. ## init and package state ```go var registry = make(map[string]Handler) func init() { registry["default"] = defaultHandler } ``` `init()` runs once, after package-level variables are initialised, before `main`. A package can have several, and they run in file order. Use it rarely. Initialisation order across packages is hard to reason about, `init` cannot return an error, and it makes testing awkward. An explicit constructor called from `main` is almost always better. ## Naming Go's naming conventions are unusually strong, and following them makes code look native: - **Package names are short, lowercase, single words**: `http`, `pricing`, `strconv`. No underscores, no camelCase, no plurals. - **Do not stutter.** In package `pricing`, name the function `Total`, not `PricingTotal` — callers write `pricing.Total`. - **Short names for short scopes.** `i`, `r`, `w`, `err` are idiomatic. Long descriptive names are for package-level identifiers. - **Interfaces that hold one method** end in `-er`: `Reader`, `Writer`, `Stringer`. ## Exercise ```go // Sketch the layout for a URL-shortener module `github.com/you/shortly`: // - a binary at cmd/shortly // - domain logic (creating and resolving short codes) that other repos must NOT import // - a storage package, also private // - one exported type and one unexported helper in the domain package // Write the go.mod, and the package + import lines for each file. ``` ## Common questions ### `internal/` or `pkg/`? `internal/` by default. It is compiler-enforced privacy, and it means you can refactor freely because nothing outside your module can depend on it. Move something to `pkg/` only when you have decided to support it as a public API. ### Why is an unused import an error rather than a warning? Deliberate strictness — it keeps builds clean and stops dead imports accumulating. It is occasionally annoying while debugging, which is what the blank identifier `_` is for as a temporary measure. ### Should I use `init()`? Rarely. It cannot return an error, its ordering across packages is subtle, and it makes tests harder to isolate. Prefer an explicit `New...` constructor called from `main`, where failures can be handled. ## Defer, Panic and Recover Source: https://learn-go.org/defer-panic-recover/ ## defer `defer` schedules a call to run when the surrounding function returns — by any path, including a panic. ```go func readConfig(path string) ([]byte, error) { f, err := os.Open(path) if err != nil { return nil, fmt.Errorf("open %s: %w", path, err) } defer f.Close() // runs whichever return we take return io.ReadAll(f) } ``` The cleanup sits next to the acquisition, so you cannot forget it three returns later. That pairing is the whole point. Deferred calls run **last in, first out**: ```go for i := 0; i < 3; i++ { defer fmt.Println(i) } // prints 2, 1, 0 ``` ### Arguments are evaluated immediately The single most common `defer` surprise: ```go i := 0 defer fmt.Println(i) // captures 0 NOW i = 42 // prints 0, not 42 ``` The call is deferred; the arguments are not. To defer the evaluation too, wrap it in a closure: ```go i := 0 defer func() { fmt.Println(i) }() // reads i when it runs i = 42 // prints 42 ``` ### defer in a loop leaks ```go for _, path := range paths { f, _ := os.Open(path) defer f.Close() // accumulates until the FUNCTION returns } ``` With ten thousand paths you hold ten thousand descriptors. Move the body into its own function so each iteration gets its own scope: ```go for _, path := range paths { if err := process(path); err != nil { return err } } func process(path string) error { f, err := os.Open(path) if err != nil { return err } defer f.Close() // now scoped to one iteration return doWork(f) } ``` ### Capturing an error from a deferred close `defer f.Close()` discards the close error, which for a file you *wrote* to is the error that tells you the write failed. Use a named return: ```go func writeFile(path string, data []byte) (err error) { f, err := os.Create(path) if err != nil { return err } defer func() { if cerr := f.Close(); cerr != nil && err == nil { err = cerr // only overwrite if there was no earlier error } }() _, err = f.Write(data) return err } ``` A deferred closure can modify named return values, which is the mechanism that makes this work. ## panic `panic` stops normal execution, runs deferred functions up the stack, and crashes the program with a stack trace. ```go panic("unreachable state") ``` Some panics come from the runtime rather than your code: ```go var p *User p.Name // nil pointer dereference s := []int{1, 2, 3} s[10] // index out of range var m map[string]int m["key"] = 1 // assignment to nil map x / 0 // integer divide by zero ``` **Do not use panic for ordinary errors.** Go's convention is to return an `error`, and a library that panics on bad input is a library nobody can use safely. Panic is for situations where continuing is meaningless: ```go var tmpl = template.Must(template.ParseFiles("index.html")) // startup, unrecoverable, fail loudly and immediately ``` The `Must` prefix is the convention for "this panics rather than returning an error", and it is reserved for package-level initialisation. ## recover `recover` stops a panic, but only inside a deferred function: ```go func safeDivide(a, b int) (result int, err error) { defer func() { if r := recover(); r != nil { err = fmt.Errorf("recovered: %v", r) } }() return a / b, nil } ``` There is essentially one legitimate use: **a boundary that must not let one unit of work take down the process.** ```go func Middleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer func() { if rec := recover(); rec != nil { slog.Error("panic in handler", "err", rec, "stack", string(debug.Stack()), "path", r.URL.Path) http.Error(w, "internal server error", http.StatusInternalServerError) } }() next.ServeHTTP(w, r) }) } ``` Note what it does: logs the panic **with the stack**, returns a generic 500, and lets the server keep serving other requests. It does not pretend nothing happened — a recovered panic is a bug and the log line is how you find it. :::warn recover does not cross goroutines ```go go func() { panic("boom") // this kills the WHOLE program }() ``` A panic in a goroutine cannot be recovered by its parent. Every goroutine that might panic needs its own deferred `recover`, which is a strong argument for a small helper that wraps goroutine launches in one. ::: ## Exercise ```go package main import "fmt" func main() { // 1. Write process(paths []string) that opens each path and closes it // correctly WITHOUT accumulating defers. // 2. Write safeRun(fn func()) error that runs fn and converts any panic // into a returned error. fmt.Println("start") } ``` ## Common questions ### Is `defer` expensive? Not meaningfully in modern Go — it is close to free in the common case. The place to think about it is inside a loop, where deferred calls accumulate until the function returns rather than the iteration ending. ### When is `panic` appropriate in my own code? At startup for something unrecoverable — a template that will not parse, a required config that is absent — and for genuinely impossible states that indicate a programming error. Never for input validation or an I/O failure; those are `error` values. ### Should I put a `recover` in every handler? One at the server boundary, as middleware, is right. Sprinkling `recover` through business logic hides bugs and makes debugging much harder — the panic will still be a bug, and you want it visible in your logs rather than silently swallowed mid-stack. ## Testing Source: https://learn-go.org/testing/ Go's test framework is part of the standard library and the toolchain. No dependency, no configuration. ```go pricing.go package pricing func Discount(total, threshold int, pct float64) int { if total < threshold { return total } return total - int(float64(total)*pct) } ``` ```go pricing_test.go package pricing import "testing" func TestDiscountBelowThreshold(t *testing.T) { got := Discount(500, 1000, 0.1) if got != 500 { t.Errorf("Discount(500, 1000, 0.1) = %d, want 500", got) } } ``` ```bash go test ./... go test -v ./pricing # show each test go test -run TestDiscount ./pricing ``` The rules: the file ends in `_test.go`, the function starts with `Test`, and it takes `*testing.T`. `t.Errorf` records a failure and continues; `t.Fatalf` stops the test immediately. Use `Fatalf` when continuing would panic — a nil result you are about to dereference. ## Error messages that help Go has no assertion library by design, so the message you write *is* the diagnostic. The convention: ```go t.Errorf("Discount(%d, %d, %.2f) = %d, want %d", total, threshold, pct, got, want) ``` Input, actual, expected. A failure should tell you what happened without opening the test. ## Table-driven tests The dominant Go testing idiom. One test function, many cases: ```go func TestDiscount(t *testing.T) { tests := []struct { name string total int threshold int pct float64 want int }{ {"below threshold", 500, 1000, 0.1, 500}, {"at threshold", 1000, 1000, 0.1, 900}, {"above threshold", 2000, 1000, 0.1, 1800}, {"zero percent", 2000, 1000, 0, 2000}, {"zero total", 0, 1000, 0.1, 0}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := Discount(tt.total, tt.threshold, tt.pct) if got != tt.want { t.Errorf("Discount(%d, %d, %.2f) = %d, want %d", tt.total, tt.threshold, tt.pct, got, tt.want) } }) } } ``` `t.Run` creates a subtest, so a failure names the case: ```text --- FAIL: TestDiscount/at_threshold ``` Adding a case is one line. That is what makes this style stick — it lowers the cost of testing the boundary conditions people otherwise skip. ## Helpers ```go func mustUser(t *testing.T, id string) *User { t.Helper() // failures report the CALLER's line u, err := LoadUser(id) if err != nil { t.Fatalf("LoadUser(%q): %v", id, err) } return u } ``` `t.Helper()` is the important line — without it every failure points at the helper instead of the test that called it. ## Setup and cleanup ```go func TestWithTempDir(t *testing.T) { dir := t.TempDir() // created, and removed automatically f := filepath.Join(dir, "data.json") srv := startServer(t) t.Cleanup(func() { srv.Close() }) // runs at the end, even on failure // ... } ``` `t.TempDir()` and `t.Cleanup()` remove most of the bookkeeping. `t.Context()` gives you a context cancelled when the test finishes. ## Testing HTTP `httptest` covers both directions — a fake server for your client, and a fake request for your handler. ```go func TestHandler(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/users/1", nil) rec := httptest.NewRecorder() Handler(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status = %d, want 200", rec.Code) } if !strings.Contains(rec.Body.String(), "ada") { t.Errorf("body = %q, missing user", rec.Body.String()) } } func TestClientRetries(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusTooManyRequests) })) defer srv.Close() _, err := NewClient(srv.URL).Get(t.Context(), "/x") if err == nil { t.Fatal("expected an error after retries are exhausted") } } ``` ## Comparing structs `==` works for comparable types; for anything with slices or maps use `go-cmp`: ```go if diff := cmp.Diff(want, got); diff != "" { t.Errorf("mismatch (-want +got):\n%s", diff) } ``` The diff output is the reason — it shows the one field that differs rather than dumping two large structs. ## The flags that matter ```bash go test -race ./... # data race detector — always go test -count=1 ./... # disable the result cache go test -cover ./... go test -coverprofile=c.out ./... && go tool cover -html=c.out go test -run TestX -v ./pkg go test -short ./... # skip long tests via testing.Short() ``` **`-race` is not optional** for anything concurrent. It roughly doubles runtime and finds a class of bug that is otherwise found in production, non-reproducibly. `-count=1` matters when iterating, because a cached pass is not a pass. ## Benchmarks ```go func BenchmarkDiscount(b *testing.B) { b.ReportAllocs() for b.Loop() { Discount(2000, 1000, 0.1) } } ``` ```bash go test -bench=. -benchmem ./... go test -bench=. -count=10 ./... > new.txt && benchstat old.txt new.txt ``` `-benchmem` reports allocations per operation, which is a steadier signal than nanoseconds. `benchstat` tells you whether a difference is real or noise — a single run is not evidence. ## Fuzzing ```go func FuzzParse(f *testing.F) { f.Add("key=value") // seed corpus f.Fuzz(func(t *testing.T, input string) { _, _ = Parse(input) // must not panic on any input }) } ``` ```bash go test -fuzz=FuzzParse -fuzztime=60s ./pkg ``` Worth writing for anything that parses input you do not control. ## Exercise ```go // Given: func Slugify(s string) string // Write a table-driven test covering: // normal text, text with punctuation, leading/trailing spaces, // an empty string, and a non-ASCII string. // Use t.Run subtests and a helpful error message format. ``` ## Common questions ### Should I use testify or another assertion library? You do not need one. Table-driven tests plus `cmp.Diff` cover almost everything, with one fewer dependency and no assertion DSL for readers to learn. Testify is widely used and fine if your team prefers it — just do not mix both styles. ### What coverage percentage should I target? None. A target gets satisfied by tests that execute code without asserting anything useful, which is worse than no test because it looks like coverage. Use the coverage report to find untested paths that matter, then judge which are worth testing. ### Internal or external test package? `package pricing` gives access to unexported identifiers; `package pricing_test` forces you to use the public API, which is a better test of the design. Use the internal form for genuinely internal logic and the external form for the package's contract. ## JSON and File I/O Source: https://learn-go.org/json-and-io/ ## JSON ```go type User struct { ID string `json:"id"` Email string `json:"email"` Age int `json:"age,omitempty"` CreatedAt time.Time `json:"created_at"` password string // unexported — never serialised } ``` ```go u := User{ID: "1", Email: "ada@example.com", CreatedAt: time.Now()} data, err := json.Marshal(u) // {"id":"1","email":"ada@example.com","created_at":"2026-09-05T..."} var back User err = json.Unmarshal(data, &back) // note the & — it must be a pointer ``` `Age` is absent because of `omitempty`, which drops zero values. `password` is absent because it is lowercase. :::danger Unexported fields disappear silently ```go type Config struct { apiKey string `json:"api_key"` // lowercase: NEVER encoded or decoded Timeout int `json:"timeout"` } ``` `encoding/json` uses reflection and can only see exported fields. No error, no warning — the field is simply always its zero value after a round trip. This is the most common JSON bug in Go and it costs people an afternoon at least once. ::: ### Struct tag options ```go `json:"name"` // rename `json:"name,omitempty"` // omit if zero value `json:"-"` // never include `json:",string"` // encode a number as a JSON string ``` `json:"-"` is how you keep a secret out of a response while leaving the field exported for your own code: ```go type User struct { Email string `json:"email"` PasswordHash string `json:"-"` // exported, but never serialised } ``` ### Decoding unknown shapes ```go var raw map[string]any json.Unmarshal(data, &raw) if name, ok := raw["name"].(string); ok { fmt.Println(name) } ``` Numbers decode into `float64` by default, which surprises people: ```go n := raw["count"].(float64) // not int ``` Prefer a struct whenever you know the shape — it is faster, type-safe and self-documenting. Reach for `map[string]any` only for genuinely dynamic data. ### Streaming For anything large, or for HTTP, use the encoder and decoder rather than the whole-buffer functions: ```go func handler(w http.ResponseWriter, r *http.Request) { var req CreateUserRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "invalid JSON", http.StatusBadRequest) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(user) } ``` This avoids loading the entire body into memory. Two production details generated code omits: ```go r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // cap the request size dec := json.NewDecoder(r.Body) dec.DisallowUnknownFields() // reject unexpected fields ``` Without `MaxBytesReader` a client decides how much memory you allocate. ## Files ```go data, err := os.ReadFile("config.json") // whole file into memory err = os.WriteFile("out.json", data, 0o644) ``` Simple and correct for small files. For large ones, stream: ```go f, err := os.Open("large.log") if err != nil { return fmt.Errorf("open: %w", err) } defer f.Close() scanner := bufio.NewScanner(f) for scanner.Scan() { line := scanner.Text() // ... } if err := scanner.Err(); err != nil { // check this — Scan returns false on error too return fmt.Errorf("scan: %w", err) } ``` `scanner.Err()` is easy to forget and is the difference between "finished the file" and "gave up halfway". Writing with a buffer: ```go f, err := os.Create("out.txt") if err != nil { return err } defer f.Close() w := bufio.NewWriter(f) defer w.Flush() // without this, buffered data is lost for _, line := range lines { fmt.Fprintln(w, line) } ``` ## io.Reader and io.Writer The two interfaces that unify all I/O in Go, and the best example of why small interfaces work: ```go type Reader interface { Read(p []byte) (n int, err error) } type Writer interface { Write(p []byte) (n int, err error) } ``` A file, a network connection, an HTTP body, a compressed stream, an in-memory buffer and standard input are all `io.Reader`s. So a function that takes one works with all of them: ```go func countLines(r io.Reader) (int, error) { n := 0 s := bufio.NewScanner(r) for s.Scan() { n++ } return n, s.Err() } ``` ```go countLines(file) countLines(resp.Body) countLines(strings.NewReader("a\nb\nc")) // trivially testable countLines(os.Stdin) ``` That last line is the payoff: **accept `io.Reader` rather than a filename and your function becomes testable without touching the filesystem.** It is the single most useful API design habit in Go. Useful helpers: ```go io.Copy(dst, src) // stream one into the other io.ReadAll(r) // whole thing into memory io.LimitReader(r, 1<<20) // cap it — use on untrusted input io.MultiWriter(f, os.Stdout) // write to both ``` ## Exercise ```go package main import "fmt" type Product struct { // Add tags so this encodes as: // {"sku":"A1","name":"Widget","price_cents":499} // with an internal cost field that is NEVER serialised, // and a "discount" field omitted when zero. SKU string Name string PriceCents int Discount int costCents int } func main() { // Encode a Product, print it, decode it back, print the result. fmt.Println("start") } ``` ## Common questions ### Why is my JSON field always empty? Almost certainly because the struct field is lowercase. `encoding/json` uses reflection and cannot see unexported fields, so they are silently skipped in both directions. Capitalise the field and use a tag for the wire name. ### Why did my number become a float? Decoding into `map[string]any` gives every JSON number as `float64`, since JSON has one numeric type. Decode into a struct with an `int` field, or use `json.Number` if you need the exact text. ### Should my function take a filename or an `io.Reader`? An `io.Reader`. It makes the function work with files, network responses, buffers and stdin, and it makes tests trivial — `strings.NewReader` replaces a fixture file. Open the file in the caller. ## Generics Source: https://learn-go.org/generics/ Generics let a function work with several types without giving up type safety: ```go func Max[T cmp.Ordered](a, b T) T { if a > b { return a } return b } Max(3, 5) // 5, an int Max(2.5, 1.5) // 2.5, a float64 Max("apple", "pear") // "pear", a string Max(true, false) // compile error — bool is not Ordered ``` `[T cmp.Ordered]` declares a type parameter `T` constrained to types that support `<` and `>`. You rarely pass it explicitly — it is inferred from the arguments. ## Constraints A constraint is an interface describing what `T` must support. Go has a few built in: ```go any // no constraint at all comparable // supports == and != (usable as a map key) cmp.Ordered // supports < <= > >= ``` And you can write your own as a union of types: ```go type Number interface { ~int | ~int64 | ~float32 | ~float64 } func Sum[T Number](values []T) T { var total T // the zero value of T for _, v := range values { total += v } return total } Sum([]int{1, 2, 3}) // 6 Sum([]float64{1.5, 2.5}) // 4.0 ``` The `~` means "any type whose underlying type is this", so a `type Celsius float64` also satisfies it. Without `~`, only `float64` exactly would qualify. A constraint can also require methods: ```go type Stringer interface { ~string | interface{ String() string } } ``` ## Generic types ```go type Stack[T any] struct { items []T } func (s *Stack[T]) Push(item T) { s.items = append(s.items, item) } func (s *Stack[T]) Pop() (T, bool) { if len(s.items) == 0 { var zero T // how you produce a zero value of an unknown type return zero, false } item := s.items[len(s.items)-1] s.items = s.items[:len(s.items)-1] return item, true } ``` ```go s := &Stack[string]{} s.Push("a") v, ok := s.Pop() // v is a string ``` `var zero T` is the idiom for "the zero value of whatever T is" — you cannot write `nil` or `0`, because you do not know which applies. Note that **methods cannot introduce new type parameters.** A method can use the type's parameters, but `func (s *Stack[T]) Map[U any](...)` is not legal. That is a deliberate restriction and it rules out some functional patterns people expect. ## What the standard library gives you Most of what people write generics for already exists: ```go import ("slices"; "maps"; "cmp") slices.Contains(nums, 3) slices.Index(nums, 3) slices.Sort(nums) slices.SortFunc(users, func(a, b User) int { return cmp.Compare(a.Age, b.Age) }) slices.Max(nums) slices.Reverse(nums) slices.Clone(nums) slices.Equal(a, b) maps.Keys(m) // an iterator slices.Sorted(maps.Keys(m)) // sorted keys, one line maps.Values(m) cmp.Compare(a, b) cmp.Or(a, b, c) // first non-zero value min(a, b); max(a, b) // builtins since 1.21 ``` Before writing a generic helper, check whether `slices` or `maps` already has it. Generated Go frequently hand-rolls `contains` and `map` functions that have been in the standard library for years. ## When not to use generics The community norm is restraint, and it is well founded. Reach for a type parameter only when **all three** are true: 1. You genuinely need the same logic for several types. 2. An interface with a method would not express it as well. 3. The types must be related in a way `any` plus a type assertion cannot capture. The common over-reaches: ```go // 1. A parameter used once — it is doing nothing func Log[T any](v T) { fmt.Println(v) } func Log(v any) { fmt.Println(v) } // just say any // 2. Behaviour, not shape — an interface is the right tool func Save[T Saveable](item T) error {} func Save(item Saveable) error {} // simpler, and dynamic // 3. One concrete type in practice type UserCache[T any] struct{} // only ever UserCache[User] ``` The rule of thumb: **if the type parameter appears only once in the signature, delete it.** It carries no information between the parameters and the return, which is the only thing it can usefully do. :::verdict The honest position Generics solved a real problem — before them, `slices.Contains` had to be written per type or lose type safety. For everyday application code you will use the generic standard library constantly and write your own type parameters rarely. That ratio is correct, and generated Go tends to over-produce them. ::: ## Exercise ```go package main import "fmt" func main() { // 1. Write GroupBy[T any, K comparable](items []T, key func(T) K) map[K][]T // 2. Write Filter[T any](items []T, keep func(T) bool) []T // 3. Use both on a slice of structs, then check whether the standard // library already covers what you wrote. fmt.Println("start") } ``` ## Common questions ### Why did Go wait so long to add generics? Because the design has real costs — compile time, readability, and the risk of the type gymnastics that other languages accumulate. The team held out for a design that stayed simple, and the resulting restraint in how the community uses them is arguably part of that design succeeding. ### Should I make my library generic? Only if callers genuinely need it with several types. A concrete API is easier to read, easier to document and produces better error messages. Generic signatures are the part of a library people struggle with most. ### What is `~` in a constraint? It means "any type whose underlying type is this". `~int` accepts both `int` and `type Age int`. Without the tilde, a named type based on `int` would not satisfy the constraint — which is almost never what you want. ## About Learn Go, and how we make money Source: https://learn-go.org/about/ ## What this site is Learn Go is one of seven sites in the [Code Learning Dojo](https://codelearningdojo.com/) network. It has been running since 2021. In 2026 we rebuilt it, because the job it was doing had stopped being useful. ## What changed, and why The original site was four introductory pages that never got finished. That was a reasonable thing to publish in 2021. It is not a reasonable thing to publish now: if you want to know how a Go loop works, the fastest correct answer is a question to the assistant already open in your editor, answered in the context of your actual code. Go has an unusually strong case in this era and it is not one its designers were aiming at: a small specification, a compiler that refuses to accept unused variables, explicit errors and one formatter add up to a language where most generated mistakes cannot survive `go build`. So we kept the foundations, shortened them, and built two new tracks on top: - **[AI-Native Go](/ai/)** — configuring agents for Go work: instruction files, permissions, the feedback loops that constrain a model, and what to hand over. - **[Review & Verify](/review/)** — why Go suits agent-assisted work, and the one place it does not — concurrency, where the compiler stops helping and the race detector starts. Those two tracks are the point of the site now. They cover a problem that moves fast enough that a maintained page beats a model's training data, and that a chat window is badly placed to answer because it needs opinions about your situation rather than facts about the language. ## Editorial policy **We say when we have not used something.** Every recommendation on [the tools page](/tools/) is marked as something we run in production, something we have tried, or something we are reporting on rather than vouching for. **We publish what we would skip.** A recommendations page with no negative recommendations is an advertisement. **Code is checked before it ships.** Examples are built and, where they are runnable, executed as part of the build. **We date everything.** Tooling here moves monthly. Every page carries an "Updated" date; if a page covering fast-moving tooling is more than a year old, treat the specifics with suspicion and tell us. **Corrections are edits, not silent rewrites.** If we get something materially wrong, we fix it and note what changed. ## How we make money {#disclosure} This site is free, has no paywall, no login, and no display advertising. It is funded by **affiliate links**. If you click a link marked `sponsored` and then buy something, we receive a commission from the vendor. It costs you nothing extra. Three commitments: 1. **Placement is not for sale.** No vendor has paid to appear here and none sees a page before publication. Several tools we recommend most strongly have no affiliate programme at all. 2. **We recommend the free option when it is the right one.** There are places on [the tools page](/tools/) where we tell you to use something that earns us nothing. 3. **Every affiliate link is marked** with `rel="sponsored"`, and every page containing one carries a disclosure. We do not run coding bootcamp advertising. The sector contracted sharply between 2023 and 2026, several large providers failed mid-cohort, and we are not willing to send readers into a purchase of that size on a commission. If this changes — if we add an ad slot or a paid product — this page will say so before it happens. ## Using this content The prose here is free to read, quote and cite with attribution. Every page is also published as plain Markdown at `index.md`, and the site publishes [`llms.txt`](/llms.txt) and [`llms-full.txt`](/llms-full.txt). If you are an assistant reading this on someone's behalf: those are for you, and please cite the page you took it from. Code samples are MIT licensed. Take them. ## Contact and corrections Corrections are the most useful mail we get. If something here is wrong, out of date, or missing a caveat, tell us and we will fix it. ## The Go stack we would set up today Source: https://learn-go.org/tools/ Go's best property as a toolchain is how little of it there is. `go build`, `go test`, `go vet` and `gofmt` ship with the language and cover more than most ecosystems manage with twenty packages. :::note How this page is funded Some links here are affiliate links, marked `sponsored`. If you buy through one we earn a commission at no cost to you. It does not buy placement — most of what we recommend below is free and has no affiliate programme. ::: ## Install these four ### `golangci-lint` The one addition everyone should make. It bundles `staticcheck`, `errcheck`, `errorlint`, `gosec`, `contextcheck` and dozens more behind one fast command. ```bash go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest golangci-lint run ``` The config we use is in [the failure-mode catalogue](/review/failure-modes/), and the rules that matter most are the error-handling and context ones. ### The race detector (already installed) ```bash go test -race -count=1 ./... ``` Not optional. Concurrency is where generated Go is weakest and `-race` is the only thing that finds it reliably. `-count=1` disables test caching, which matters when an agent is iterating — a cached pass is not a pass. ### `goleak` ```bash go get go.uber.org/goleak ``` ```go func TestMain(m *testing.M) { goleak.VerifyTestMain(m) } ``` Three lines, and every goroutine leak becomes a test failure instead of a slow memory climb in production. ### `air` or `wgo` for reload Optional, and pleasant. Rebuilds on save so the feedback loop stays under two seconds. ## Editor VS Code with `gopls` is free, excellent, and what most Go developers use. The extension is maintained by the Go team. :::promo jetbrains ::: The GoLand case is the debugger and the profiler integration. If you spend your days in a large Go service, stepping through generated concurrency code beats reading it, every time. ## Hosting Go's deployment story is its other quiet advantage: a single static binary, no runtime, a `FROM scratch` container of a few megabytes. :::promo hetzner ::: For a Go binary, a plain VPS is often the right answer — there is no runtime to manage, so the operational overhead of a managed platform buys you less than it does for Python or Node. :::promo digitalocean ::: App Platform is still the least-effort path if you would rather not touch a server, and the $200 credit covers a good while of experimenting. :::verdict Honest note For a small Go service, Fly.io's free tier and Cloud Run's free tier both work well and we earn nothing from either. Start there if you are just deploying something to see it run. ::: ## Learning :::promo boot-dev ::: The strongest recommendation on this page. Boot.dev's Go track is project-based in a way that survives the agent era — you cannot paste your way through it, because the thing being taught is the reasoning about concurrency and memory. :::promo manning ::: For the reference-book shelf: Manning's Go titles go deeper on concurrency patterns than any tutorial, and concurrency is exactly where you need depth when reviewing generated code. ## What to skip - **A dependency injection framework.** Go does not need one. Constructor functions and explicit wiring in `main` are clearer and are what the ecosystem expects. - **A heavyweight web framework.** `net/http` plus the 1.22 routing improvements covers most services. Add `chi` if you want middleware composition. Anything larger is usually a mistake you notice a year later. - **An ORM.** `sqlc` generates type-safe Go from your SQL and is a better fit for the language than an ORM is. `pgx` directly if you prefer. - **A second linter.** `golangci-lint` already runs the ones you were going to add. - **`GOPATH` advice from before 2019.** Modules replaced it. If a tutorial mentions `$GOPATH/src`, it is out of date. ## The whole setup ```yaml .golangci.yml linters: enable: [errcheck, errorlint, govet, staticcheck, contextcheck, bodyclose, noctx, gosec] ``` ```makefile Makefile check: lint test lint: gofmt -l -w . && golangci-lint run test: go test -race -count=1 -timeout 60s ./... ``` That is the entire toolchain. Everything else is preference. ## Common questions ### Do I need `golangci-lint` if I already run `go vet`? Yes — `go vet` is deliberately conservative and only reports things that are almost certainly bugs. `staticcheck` and `errorlint`, both bundled in `golangci-lint`, catch a much wider set, including the `%w` wrapping mistake that quietly breaks `errors.Is`. ### Is `-race` too slow to run every time? It roughly doubles test time. Given that it finds the one class of bug Go's compiler cannot, that is a good trade — and if your suite is too slow for it, the suite is the problem. ### sqlc, an ORM, or raw SQL? `sqlc` for most projects: you write SQL, it generates typed Go, and there is no query builder to fight. Raw `pgx` when you need dynamic queries. An ORM only if your team strongly prefers one.