{"slug": "golang-for-ai-developers-from-0-to-pro", "title": "🐹 Golang for AI Developers 🤖 — From 0 to Pro ⚡", "summary": "A developer has published a comprehensive guide to using Go for AI development, covering topics from basic syntax to building concurrent, observable services that front machine learning models. The guide emphasizes Go's strengths in API gateways, streaming proxies, and orchestrators, while recommending a hybrid approach where Go handles HTTP, auth, and fan-out, and Python handles heavy computation.", "body_md": "One file, one path: from\n\n`package main`\n\nto shipping a concurrent, observable Go service that fronts your models and never falls over.Every example is drawn from what AI engineers actually build in Go — streaming proxies, tool dispatchers, rate limiters, worker pools, context-cancelled model calls. No\n\n`foo`\n\n/`bar`\n\nfiller.\n\nCompanion reads: [🐍 Python for AI Developers](//python_for_ai_developers.md) (the sibling to this guide), [🏗️ Building High-Quality AI Agents](//building_high_quality_ai_agents.md), [🏢 Enterprise-Ready AI Agents](//enterprise_ready_ai_agents_guide.md).\n\n| You are… | Start at | Skip |\n|---|---|---|\n| New to Go | Part 1 → read straight through | Parts 12–13 on first pass |\n| Coming from Python | Part 1 (the phrasebook), then Part 5 and Part 6 | — |\n| Coming from Java/C# | Part 4, Part 5 — inheritance and exceptions are gone | Part 2 (skim) |\n| Building AI services | Part 6, Part 7, Part 9 | — |\n| Reviewing code | Part 14, Part 15 | everything else |\n\n**Convention:** `// ✅`\n\n= do this, `// ❌`\n\n= don't. Snippets target **Go 1.22+**, with newer-version wins called out inline.\n\nGo was designed for **large teams maintaining network services over years**. Every trade-off follows from that:\n\n| Go chose | Instead of | Consequence for you |\n|---|---|---|\n| A tiny spec (25 keywords) | Rich features | You can read any Go file after a week |\n| Compile to one static binary | Runtime + deps |\n`FROM scratch` images, 10 ms cold start |\n| Explicit errors as values | Exceptions | Failure paths are visible in the code |\n| Composition + interfaces | Inheritance | No class hierarchies to reverse-engineer |\n| Goroutines + channels | Callbacks / async colouring | Blocking code that scales to 100k connections |\n| One formatter, one toolchain | Ecosystem choice | Zero config debates; `go test` , `go fmt` , `pprof` are built in |\n\nGo is *boring on purpose*. The payoff is that a service written by someone who left two years ago still compiles, still reads clearly, and still runs.\n\n```\n[your .go files] → [compiler: types, escape analysis, inlining] → [one native binary]\n                                                                   ↑ includes the runtime\n                                                                     (scheduler + GC)\n```\n\n`COPY binary /`\n\n.The cost: more ceremony up front, no REPL, and a smaller ML ecosystem.\n\n| Dimension | Go | Python |\n|---|---|---|\n| Execution | Native binary + embedded runtime | Bytecode on the CPython VM |\n| Typing | Static, enforced by the compiler | Dynamic; static only via mypy in CI |\n| Parallelism | Real: goroutines across all cores | GIL-limited; processes or C extensions |\n| Concurrency cost | ~2 KB per goroutine | ~KB per coroutine, ~MB per thread |\n| p99 latency | Stable (GC pauses < 1 ms) | Noisier |\n| Deploy artifact | 15–40 MB static binary | Interpreter + wheels + lockfile |\n| Startup | ~5 ms | 100–500 ms (imports) |\n| ML/AI libraries | Thin (inference clients, ONNX, tokenizers) | Everything |\n| Best at | API gateways, streaming proxies, orchestrators, high-fan-out workers | Model training, data science, ML inference glue |\n\n**The production shape that wins** — and the one in this repo's [ CLAUDE.md](//CLAUDE.md) — is both: Go as the BFF that owns HTTP, auth, tenancy, streaming and fan-out; Python as the ML service it calls for heavy computation. Use Go where request volume and connection count live; use Python where the models live.\n\n| Python | Go | Note |\n|---|---|---|\n`x = 5` |\n`x := 5` |\n`:=` declares + infers, inside functions only |\n`list[int]` |\n`[]int` |\nSlice — dynamic array |\n`dict[str, int]` |\n`map[string]int` |\nIteration order is randomized\n|\n`tuple` |\nstruct, or multiple return values | No tuple type |\n`None` |\n`nil` (pointers, slices, maps, interfaces, funcs, chans) |\nValue types have zero values instead |\n`Optional[T]` |\n`*T` , or `(T, bool)` , or `(T, error)`\n|\nPointers are the \"maybe\" of Go |\n`raise ValueError(...)` |\n`return fmt.Errorf(\"...: %w\", err)` |\nErrors are returned, not thrown |\n`try/except` |\n`if err != nil { … }` |\nExplicit at every call |\n`with open(...) as f:` |\n`f, err := os.Open(...)` ; `defer f.Close()`\n|\n`defer` is the context manager |\n`@decorator` |\nHigher-order function / middleware | Wrap the function or the handler |\n`class A: def m(self)` |\n`type A struct{}` + `func (a A) M()`\n|\nMethods live outside the type |\n`Protocol` (structural) |\n`interface` |\nGo interfaces are structural too — no `implements`\n|\n`async def` / `await`\n|\njust call it, in a `go` routine |\nNo function colouring |\n`asyncio.gather` |\n`errgroup.Group` |\nBounded with `SetLimit`\n|\n`asyncio.Semaphore(8)` |\nbuffered channel or `SetLimit(8)`\n|\n|\n`f\"{x:.2f}\"` |\n`fmt.Sprintf(\"%.2f\", x)` |\n|\n`pytest` |\n`go test ./...` |\nTesting is in the stdlib |\n`venv` + `pyproject.toml`\n|\n`go.mod` |\nModules, no activation |\n\n```\npackage main\n\nimport (\n    \"fmt\"\n    \"log/slog\"\n    \"net/http\"\n    \"os\"\n)\n\nfunc main() {\n    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))\n    mux := http.NewServeMux()\n    mux.HandleFunc(\"GET /healthz\", func(w http.ResponseWriter, r *http.Request) {\n        fmt.Fprintln(w, \"ok\")\n    })\n    logger.Info(\"listening\", \"addr\", \":8080\")\n    if err := http.ListenAndServe(\":8080\", mux); err != nil {\n        logger.Error(\"server failed\", \"err\", err)\n        os.Exit(1)\n    }\n}\n```\n\nThree things a Python developer should notice: no framework, no decorators, and errors returned rather than raised. (`\"GET /healthz\"`\n\nmethod-and-pattern routing is Go 1.22+.)\n\n🎯 Actionable rules\n\n- Choose Go for the request path and the fan-out; keep Python where the models are.\n- Let the compiler carry the weight you spend mypy effort on in Python.\n- Learn\n`error`\n\n,`interface`\n\n,`defer`\n\n, and`context`\n\n— everything else is syntax.\n\n``` js\nvar name string          // \"\" — declared variables are ALWAYS initialized\nvar count int            // 0\nvar ratio float64        // 0\nvar ok bool              // false\nvar tools []string       // nil (usable: len 0, append works)\nvar index map[string]int // nil (readable, but WRITING panics)\nvar client *http.Client  // nil\n\nmodel := \"claude-opus-5\"           // := infers the type; functions only\ntimeout, retries := 30, 3          // multiple assignment\n_, err := doThing()                // _ discards a value you must accept\n```\n\n**Zero values are Go's answer to None.** There is no uninitialized memory, so a struct is useful the moment it exists. Design your types so the zero value works (\n\n`sync.Mutex`\n\n, `bytes.Buffer`\n\n, and `http.Client`\n\nall do).⚠️ `var m map[string]int`\n\nis nil: reads return the zero value, writes panic. Always `m := make(map[string]int)`\n\nor `m := map[string]int{}`\n\n.\n\n```\nint, int8/16/32/64, uint…    // int is 64-bit on modern platforms; use it by default\nfloat32, float64             // float64 unless you're storing millions of embeddings\nstring                       // immutable, UTF-8 bytes\nbyte  = uint8                // a raw byte\nrune  = int32                // one Unicode code point\nbool\n[]T, map[K]V, chan T, *T, func(...) ..., interface{ … }, struct{ … }\n```\n\nGo has **no implicit conversion**, not even `int`\n\n→ `int64`\n\n:\n\n``` js\nvar i int = 42\nvar f float64 = float64(i)          // explicit, always\nvar u uint8 = uint8(300)            // ⚠️ silently wraps to 44 — check ranges yourself\nn, err := strconv.Atoi(\"42\")        // string → int (returns an error!)\ns := strconv.Itoa(42)               // int → string\nf, err := strconv.ParseFloat(\"0.7\", 64)\nb, err := strconv.ParseBool(\"true\")\n```\n\n⚠️ `string(65)`\n\ngives `\"A\"`\n\n, not `\"65\"`\n\n— it converts a code point. Use `strconv`\n\n. (`go vet`\n\nflags this.)\n\n`iota`\n\n``` js\nconst MaxHistoryTurns = 20                    // untyped: adapts to context\nconst ToolTimeout = 30 * time.Second          // typed by inference\n\ntype Role string\nconst (\n    RoleUser      Role = \"user\"\n    RoleAssistant Role = \"assistant\"\n    RoleSystem    Role = \"system\"\n)\n\ntype Status int\nconst (\n    StatusOK Status = iota   // 0 — iota counts from 0 within a const block\n    StatusRetry              // 1\n    StatusFailed             // 2\n)\n\nfunc (s Status) String() string {              // makes it print nicely everywhere\n    switch s {\n    case StatusOK:     return \"ok\"\n    case StatusRetry:  return \"retry\"\n    case StatusFailed: return \"failed\"\n    default:           return fmt.Sprintf(\"Status(%d)\", int(s))\n    }\n}\n```\n\nA named string type (`type Role string`\n\n) is Go's enum: the compiler rejects a raw `\"usr\"`\n\ntypo where a `Role`\n\nis expected, while JSON marshalling still just works.\n\nStrings are **immutable byte slices** holding UTF-8. Indexing gives bytes; ranging gives runes.\n\n```\ns := \"café\"\nlen(s)                       // 5 — BYTES, not characters\ns[0]                         // 99 (byte 'c')\nfor i, r := range s {        // i = byte offset, r = rune\n    fmt.Printf(\"%d:%c \", i, r)   // 0:c 1:a 2:f 3:é\n}\nutf8.RuneCountInString(s)    // 4 — actual character count\n[]rune(s)[3]                 // 'é' — index by character (allocates)\n[]byte(s)                    // copy to a mutable byte slice\n```\n\nThe `strings`\n\npackage covers what Python puts on `str`\n\n:\n\n```\nstrings.TrimSpace(\"  hi \\n\")            // \"hi\"\nstrings.ToLower(\"Calculate 2+2\")\nstrings.Split(\"a,b,c\", \",\")             // []string{\"a\",\"b\",\"c\"}\nstrings.SplitN(\"calculate 10*5\", \"calculate\", 2)[1]   // \" 10*5\"  (maxsplit)\nstrings.Join([]string{\"a\", \"b\"}, \", \")  // \"a, b\"\nstrings.HasPrefix(name, \"tool:\")        // also HasSuffix, Contains, EqualFold\nstrings.ReplaceAll(s, \"ok\", \"done\")\nstrings.Fields(\"  a  b \")               // [\"a\",\"b\"] — split on any whitespace\nstrings.TrimPrefix(path, \"docs/\")       // prefix-safe (not Trim, which is a char set)\nstrings.Cut(\"key=value\", \"=\")           // \"key\", \"value\", true — the modern splitter\n```\n\n**Building strings**: `+=`\n\nin a loop is O(n²) and allocates every time. Use a builder:\n\n``` js\nvar b strings.Builder\nb.Grow(len(history) * 64)                  // one allocation if you can estimate\nfor _, m := range history {\n    fmt.Fprintf(&b, \"%s: %s\\n\", m.Role, m.Content)\n}\nprompt := b.String()\n```\n\n`fmt`\n\nverbs you'll actually use\n\n```\nfmt.Sprintf(\"%s scored %.2f\", name, score)   // string, 2-decimal float\nfmt.Sprintf(\"%d/%d tokens\", used, limit)     // int\nfmt.Sprintf(\"%q\", name)                      // \"calculator\" — quoted, like Python's !r\nfmt.Sprintf(\"%v\", cfg)                       // default format\nfmt.Sprintf(\"%+v\", cfg)                      // {Name:agent Model:claude-opus-5} ← field names\nfmt.Sprintf(\"%#v\", cfg)                      // Go syntax — best for debugging\nfmt.Sprintf(\"%T\", v)                         // the dynamic type: *main.Agent\nfmt.Errorf(\"run tool %q: %w\", name, err)     // %w WRAPS an error (see §5)\n```\n\n`%q`\n\nis your `!r`\n\n: it makes `\"\"`\n\nand `\" \"`\n\nvisible in logs. `%+v`\n\non a struct is the fastest debugging tool in the language.\n\nA slice is a 3-word header: **pointer to a backing array, length, capacity**. That header is copied on assignment; the array is not.\n\n```\nxs := []string{\"a\", \"b\"}          // literal\nys := make([]string, 0, 100)      // len 0, cap 100 — preallocate when you know the size\nys = append(ys, \"x\")              // append RETURNS a new header; always reassign\nlen(xs); cap(xs)\nxs = append(xs, ys...)            // ... spreads a slice (like Python's *)\ncopy(dst, src)                    // copies min(len(dst), len(src))\nlast10 := history[max(0, len(history)-10):]   // sliding window (min/max builtins: Go 1.21+)\n```\n\n⚠️ **The aliasing trap** — slicing shares the backing array:\n\n```\nall := []int{1, 2, 3, 4, 5}\nhead := all[:3]\nhead = append(head, 99)      // cap allows it → OVERWRITES all[3]\nfmt.Println(all)             // [1 2 3 99 5]\n```\n\nFixes: three-index slicing to cap it (`all[:3:3]`\n\nforces `append`\n\nto copy), or `slices.Clone(head)`\n\n.\n\n⚠️ **Never keep a small slice of a huge one** — the whole backing array stays alive:\n\n```\nsnippet := slices.Clone(bigDoc[:100])   // ✅ 100 bytes retained, not 50 MB\n```\n\nThe `slices`\n\npackage (Go 1.21+) replaces most hand-written loops:\n\n```\nslices.Contains(tools, \"bash\")\nslices.Sort(scores)\nslices.SortFunc(docs, func(a, b Doc) int { return cmp.Compare(b.Score, a.Score) })  // desc\nslices.Index(names, \"calculator\")\nslices.Clone(xs); slices.Reverse(xs); slices.Max(scores)\nscores := map[string]float64{\"calculator\": 0.94}\nv := scores[\"missing\"]                 // 0 — no error, zero value\nv, ok := scores[\"missing\"]             // ✅ the comma-ok idiom: v=0, ok=false\ndelete(scores, \"calculator\")\nlen(scores)\nclear(scores)                          // Go 1.21+\n\nfor k, v := range scores { … }         // ⚠️ ORDER IS RANDOMIZED, deliberately\nkeys := slices.Sorted(maps.Keys(scores))   // Go 1.23+ — deterministic iteration\n```\n\n`dict.get`\n\nvs `[]`\n\n.`sync.RWMutex`\n\nor use `sync.Map`\n\n(only for its two specific patterns — see §6.6).`make(map[string]int, 1000)`\n\n.\n\n```\ntype AgentConfig struct {\n    Name        string   `json:\"name\"`\n    Model       string   `json:\"model\"`\n    Temperature float64  `json:\"temperature,omitempty\"`\n    Tools       []string `json:\"tools,omitempty\"`\n    apiKey      string   `json:\"-\"`     // lowercase = unexported; \"-\" = never marshalled\n}\n\ncfg := AgentConfig{Name: \"researcher\", Model: \"claude-opus-5\"}   // ✅ field names, always\np := &cfg                       // pointer\np.Temperature = 0.2             // auto-dereference — no -> in Go\nfmt.Printf(\"%+v\\n\", cfg)\n```\n\n**Exported = capitalized.** `Name`\n\nis visible outside the package; `apiKey`\n\nis not. That single rule replaces `public`\n\n/`private`\n\n.\n\n**Struct tags** are metadata read by reflection — the JSON, DB, and validation layers all use them.\n\n**Value or pointer?**\n\n| Use a value | Use a pointer |\n|---|---|\nSmall, immutable-ish (`time.Time` , `Point` ) |\nThe method mutates the receiver |\nYou want a copy (concurrency safety) |\nThe struct is large (copying costs) |\n| Zero value is meaningful | Nil must be distinguishable from empty |\n\nGo is **always pass-by-value** — passing a struct copies it; passing a pointer copies the pointer. Slices, maps, and channels contain internal pointers, so copying the header still shares the data.\n\n```\nif err := run(ctx); err != nil {          // ✅ init statement scopes err to the if\n    return fmt.Errorf(\"run: %w\", err)\n}\n\nswitch {                                   // no condition = cleaner if/else-if chain\ncase score > 0.9:  label = \"high\"\ncase score > 0.5:  label = \"medium\"\ndefault:           label = \"low\"\n}\n\nswitch status {                            // no fallthrough by default (unlike C)\ncase StatusOK, StatusRetry:                // multiple values per case\n    continue\n}\n\nfor i := 0; i < n; i++ { }                 // classic\nfor i, msg := range history { }            // range: index+value\nfor _, msg := range history { }            // value only\nfor k := range scores { }                  // map: keys only\nfor range 5 { }                            // Go 1.22+: repeat N times\nfor { break }                              // infinite loop — the only `while`\n\nfor msg := range ch { }                    // range over a channel until it's closed\nfor tok := range stream.Tokens() { }       // Go 1.23+: range over an iterator function\n```\n\nThere is no `while`\n\n, no ternary, and no `do/while`\n\n. That's not an oversight — it's the \"one obvious way\" principle.\n\n⚠️ `range`\n\ncopies each element: `for _, d := range docs { d.Score = 0 }`\n\nmutates a copy. Use `for i := range docs { docs[i].Score = 0 }`\n\n.\n\n✅ Since **Go 1.22**, loop variables are **per-iteration**, so the classic \"all goroutines see the last value\" bug is gone. On older versions you needed `i := i`\n\ninside the loop.\n\n`goto`\n\nexists; you will not use it. Labeled `break`\n\n/`continue`\n\nare occasionally right for breaking out of nested loops:\n\n```\nouter:\nfor _, doc := range docs {\n    for _, chunk := range doc.Chunks {\n        if chunk.Match(q) { break outer }\n    }\n}\n```\n\n🎯 Actionable rules\n\n- Design types so the zero value is useful; never return a nil map you expect callers to write to.\n- Always reassign the result of\n`append`\n\n, and`slices.Clone`\n\nanything you retain from a big slice.- Use comma-ok on map reads whenever \"absent\" and \"zero\" differ.\n`%+v`\n\nand`%q`\n\nin every debug print;`%w`\n\nin every wrapped error.\n\n`defer`\n\n```\n// Summarize returns a summary of text capped at maxWords words.\n//\n// It collapses whitespace and never splits a word. maxWords must be > 0.\nfunc Summarize(text string, maxWords int) (string, error) {\n    if maxWords <= 0 {\n        return \"\", fmt.Errorf(\"maxWords must be positive, got %d\", maxWords)\n    }\n    words := strings.Fields(text)\n    if len(words) > maxWords {\n        words = words[:maxWords]\n    }\n    return strings.Join(words, \" \"), nil\n}\n\nsummary, err := Summarize(doc, 50)\nif err != nil { … }\n```\n\n** (T, error) is the signature of Go.** The error is the last return value, always. There is no\n\n`Optional`\n\n, no exception, no hidden control flow.**Doc comments** start with the identifier's name and are the package's documentation (`go doc`\n\n, pkg.go.dev). Exported identifiers without a comment are flagged by linters — and the comment is what an LLM reads when your function becomes a tool.\n\n```\nfunc splitHostPort(s string) (host string, port int, err error) {   // named returns\n    // … named results are pre-declared and zero-valued; a bare `return` returns them\n    return host, port, nil                     // ✅ still return explicitly for clarity\n}\n```\n\nUse named returns for **documentation** and for `defer`\n\n-based error wrapping (§3.4) — not as an excuse for naked `return`\n\ns in long functions.\n\n```\nfunc RunTool(name string, args ...any) (string, error) { … }\nRunTool(\"calculator\", \"2+2\")\nRunTool(\"search\", queryArgs...)                 // spread a slice\n\ntype ToolFunc func(ctx context.Context, args json.RawMessage) (string, error)\n\nvar registry = map[string]ToolFunc{}            // string → behaviour, the Go way\n\nfunc Register(name string, fn ToolFunc) { registry[name] = fn }\n```\n\nFunctions are values: assign them, store them in maps, pass them, return them. That covers most of what Python decorators do.\n\n```\nfunc makeRetrier(attempts int, base time.Duration) func(context.Context, func() error) error {\n    return func(ctx context.Context, op func() error) error {\n        var err error\n        for i := range attempts {\n            if err = op(); err == nil {\n                return nil\n            }\n            select {\n            case <-time.After(base << i):          // exponential backoff\n            case <-ctx.Done():\n                return ctx.Err()\n            }\n        }\n        return fmt.Errorf(\"after %d attempts: %w\", attempts, err)\n    }\n}\n\nretry := makeRetrier(3, 100*time.Millisecond)\n```\n\nClosures capture variables **by reference**, so a closure can outlive the function that made it — the compiler moves those variables to the heap (see escape analysis, §7.4).\n\n`defer`\n\nin practice\n`defer`\n\nschedules a call to run when the surrounding **function** returns — on any path, including panic. It is Go's `with`\n\n/`finally`\n\n.\n\n```\nfunc fetchDoc(ctx context.Context, url string) ([]byte, error) {\n    req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)\n    if err != nil {\n        return nil, fmt.Errorf(\"fetchDoc: build request: %w\", err)\n    }\n    resp, err := http.DefaultClient.Do(req)\n    if err != nil {\n        return nil, fmt.Errorf(\"fetchDoc: %w\", err)\n    }\n    defer resp.Body.Close()          // ✅ immediately after the error check, every time\n    …\n}\n```\n\nFour rules that cover every `defer`\n\nbug:\n\n`defer`\n\ntime\n\n```\n   start := time.Now()\n   defer log.Printf(\"took %s\", time.Since(start))   // ❌ Since() runs NOW → always ~0\n   defer func() { log.Printf(\"took %s\", time.Since(start)) }()   // ✅ closure defers the read\nfor _, p := range paths {\n       f, _ := os.Open(p)\n       defer f.Close()        // ❌ 10 000 open files, all closed at the very end\n   }\n   for _, p := range paths {  // ✅ give each iteration its own function\n       func() {\n           f, _ := os.Open(p); defer f.Close(); process(f)\n       }()\n   }\nfunc (s *Store) Save(ctx context.Context, d Doc) (err error) {\n       tx, err := s.db.BeginTx(ctx, nil)\n       if err != nil { return err }\n       defer func() {\n           if err != nil { _ = tx.Rollback(); return }\n           err = tx.Commit()\n       }()\n       …\n   }\n```\n\n⚠️ Deferred `Close()`\n\non a **writer** can silently drop errors. For files you write, close explicitly and check, or capture it: `defer func() { err = errors.Join(err, f.Close()) }()`\n\n.\n\n`init()`\n\nand package-level state\n\n```\nfunc init() { … }        // runs once, after package vars, before main\n```\n\nUse it almost never: it hides work, runs on import, and makes tests order-dependent. Prefer an explicit constructor called from `main`\n\n. The one defensible use is registering a driver or a codec.\n\n🎯 Actionable rules\n\n- Return\n`(T, error)`\n\n; handle or wrap the error at the very next line.`defer`\n\nthe cleanup on the line after the error check that acquired the resource.- No\n`defer`\n\ninside loops — wrap the body in a function.- Doc-comment every exported identifier, starting with its name.\n\n```\ntype Agent struct {\n    cfg      AgentConfig\n    llm      LLMClient\n    history  []Message\n    mu       sync.Mutex\n}\n\n// NewAgent constructs an Agent. Constructor functions are Go's __init__.\nfunc NewAgent(cfg AgentConfig, llm LLMClient) (*Agent, error) {\n    if cfg.Name == \"\" {\n        return nil, errors.New(\"agent: name is required\")\n    }\n    return &Agent{cfg: cfg, llm: llm}, nil\n}\n\nfunc (a *Agent) AddMessage(role Role, content string) {   // pointer receiver: mutates\n    a.mu.Lock()\n    defer a.mu.Unlock()\n    a.history = append(a.history, Message{Role: role, Content: content})\n}\n\nfunc (a *Agent) Len() int { return len(a.history) }        // pointer for consistency\n\nfunc (c AgentConfig) Describe() string {                   // value receiver: read-only, small\n    return fmt.Sprintf(\"%s/%s@%.1f\", c.Name, c.Model, c.Temperature)\n}\n```\n\n**Receiver rules:**\n\n`sync.Mutex`\n\n(copying a mutex is a bug `go vet`\n\ncatches).`*T`\n\nsatisfies an interface when methods have pointer receivers — a plain `T`\n\nvalue won't compile. This is the #1 \"why doesn't my type implement this interface\" error.\n\n```\ntype BaseTool struct {\n    Name        string\n    Description string\n}\n\nfunc (b BaseTool) Schema() string { … }\n\ntype CalculatorTool struct {\n    BaseTool           // embedded: no field name\n    Precision int\n}\n\ncalc := CalculatorTool{BaseTool: BaseTool{Name: \"calculator\"}, Precision: 4}\ncalc.Name          // promoted field\ncalc.Schema()      // promoted method\n```\n\nEmbedding **promotes** fields and methods — it looks like inheritance but it's delegation: there is no virtual dispatch and no `super`\n\n. Embedding an *interface* is the standard way to build decorators and partial fakes:\n\n```\ntype loggingStore struct {\n    Store                     // embedded interface: unimplemented methods pass through\n    log *slog.Logger\n}\nfunc (s loggingStore) Get(ctx context.Context, id string) (Doc, error) {\n    s.log.Info(\"get\", \"id\", id)\n    return s.Store.Get(ctx, id)\n}\n```\n\nThere is no `implements`\n\nkeyword. If the method set matches, the type satisfies the interface.\n\n```\n// Defined in the package that USES it, not the one that implements it.\ntype LLMClient interface {\n    Complete(ctx context.Context, prompt string) (string, error)\n}\n\ntype AnthropicClient struct{ … }\nfunc (c *AnthropicClient) Complete(ctx context.Context, p string) (string, error) { … }\n// *AnthropicClient now satisfies LLMClient. No import of your package required.\n\nagent, _ := NewAgent(cfg, &AnthropicClient{})     // prod\nagent, _ := NewAgent(cfg, &fakeLLM{reply: \"42\"})  // test — no mocking library needed\n```\n\nThe three rules that make Go interfaces work:\n\n`io.Reader`\n\nhas one method. A 12-method interface is a class in disguise; nobody can fake it in a test.\n\n``` js\nvar _ LLMClient = (*AnthropicClient)(nil)    // compile-time assertion that it satisfies\n```\n\n`any`\n\n, type assertions, and type switches\n\n``` js\nvar v any = payload                    // any == interface{} (Go 1.18+ alias)\n\ns, ok := v.(string)                    // ✅ comma-ok: never panics\ns := v.(string)                        // ❌ panics if v isn't a string\n\nswitch x := v.(type) {                 // type switch\ncase string:\n    return x\ncase map[string]any:\n    return fmt.Sprintf(\"%d keys\", len(x))\ncase nil:\n    return \"null\"\ndefault:\n    return fmt.Sprintf(\"unsupported %T\", x)\n}\n```\n\n`any`\n\nthrows away the compiler's help — use it only at the JSON/reflection boundary and convert into a real type immediately (the same discipline as Python's `Any`\n\n).\n\n⚠️ **The typed-nil trap** — an interface holding a nil pointer is *not* nil:\n\n``` js\nfunc newClient() *AnthropicClient { return nil }\nvar c LLMClient = newClient()\nc == nil        // false! the interface has a type (*AnthropicClient) and a nil value\n```\n\nFix: return the interface type as a literal `nil`\n\n, never a typed nil pointer. Most commonly this bites with `error`\n\n— never declare `var err *MyError`\n\nand return it as `error`\n\n.\n\nType parameters (Go 1.18+) exist to remove copy-paste, not to build hierarchies.\n\n```\nfunc Map[T, U any](xs []T, f func(T) U) []U {\n    out := make([]U, 0, len(xs))\n    for _, x := range xs {\n        out = append(out, f(x))\n    }\n    return out\n}\nnames := Map(tools, func(t Tool) string { return t.Name() })\n\nfunc Keys[K comparable, V any](m map[K]V) []K { … }   // comparable = usable as a map key\n\ntype Number interface{ ~int | ~int64 | ~float64 }      // ~ = \"any type whose underlying type is\"\nfunc Sum[T Number](xs []T) T { var s T; for _, x := range xs { s += x }; return s }\n\n// A generic, type-safe cache — the common real-world use.\ntype Cache[K comparable, V any] struct {\n    mu sync.RWMutex\n    m  map[K]V\n}\nfunc NewCache[K comparable, V any]() *Cache[K, V] {\n    return &Cache[K, V]{m: make(map[K]V)}\n}\nfunc (c *Cache[K, V]) Get(k K) (V, bool) {\n    c.mu.RLock(); defer c.mu.RUnlock()\n    v, ok := c.m[k]\n    return v, ok\n}\n```\n\n**When not to use generics:** if an interface expresses it, use the interface. Generics can't have methods with their own type parameters, they inflate compile times, and `Map`\n\n/`Filter`\n\nchains read worse in Go than a plain `for`\n\nloop. The `slices`\n\n, `maps`\n\n, and `cmp`\n\npackages already cover 90% of what you'd write.\n\n| Interface | Method | Why it matters |\n|---|---|---|\n`error` |\n`Error() string` |\nEvery failure (§5) |\n`fmt.Stringer` |\n`String() string` |\nCustom formatting in every `%v`\n|\n`io.Reader` / `io.Writer`\n|\n`Read` /`Write`\n|\nFiles, sockets, buffers, HTTP bodies — all compose |\n`io.Closer` |\n`Close() error` |\nPairs with `defer`\n|\n`json.Marshaler` / `Unmarshaler`\n|\nCustom JSON | Enums, time formats, LLM payload quirks |\n`context.Context` |\n`Done` , `Err` , `Value` , `Deadline`\n|\nCancellation everywhere (§6.5) |\n`http.Handler` |\n`ServeHTTP` |\nEvery middleware in Go |\n`sort.Interface` |\n`Len` /`Less` /`Swap`\n|\nMostly superseded by `slices.SortFunc`\n|\n\n`io.Reader`\n\n/`io.Writer`\n\nare the reason Go plumbing composes so well: an HTTP body, a gzip stream, a file, and a `bytes.Buffer`\n\nare interchangeable.\n\n🎯 Actionable rules\n\n- Constructors return\n`(*T, error)`\n\n; validate there, so an existing value is always valid.- Define small interfaces in the consuming package; accept interfaces, return structs.\n`var _ Iface = (*T)(nil)`\n\nto assert satisfaction at compile time.- Reach for generics only after you've written the same function twice.\n\n```\ntype error interface {\n    Error() string\n}\n```\n\nThat's it. An error is any value with an `Error() string`\n\nmethod. There is no stack unwinding, no exception hierarchy, no invisible control flow — which is why Go code has `if err != nil`\n\neverywhere and why you can always see the failure path.\n\n```\nerrors.New(\"agent: name is required\")                       // static message\nfmt.Errorf(\"embed batch %d: %w\", i, err)                    // wrap with context\nfmt.Errorf(\"parse config: %v\", err)                         // %v = context WITHOUT wrapping\nerrors.Join(err1, err2)                                     // multiple failures (Go 1.20+)\n```\n\n`%w`\n\nvs `%v`\n\n:`%w`\n\nkeeps the original error reachable by `errors.Is`\n\n/`errors.As`\n\n; `%v`\n\nflattens it to text. Wrap by default; use `%v`\n\ndeliberately when you don't want callers coupling to an internal error type.\n\nFollow one convention across the codebase — this repo's ([ CLAUDE.md](//CLAUDE.md)) is\n\n`fmt.Errorf(\"packagename.FuncName: %w\", err)`\n\n:\n\n``` js\nfunc (r *Repo) GetDoc(ctx context.Context, id string) (Doc, error) {\n    var d Doc\n    if err := r.db.GetContext(ctx, &d, qGetDoc, id); err != nil {\n        return Doc{}, fmt.Errorf(\"repo.GetDoc: %w\", err)\n    }\n    return d, nil\n}\n```\n\nRead top-to-bottom, the final message becomes a trace:\n\n`handler.Query: service.Answer: repo.GetDoc: sql: no rows in result set`\n\nRules: add **context, not restatement** (never `\"error: %w\"`\n\n); don't capitalize or end with punctuation; never log *and* return the same error — pick one, and log at the boundary that handles it.\n\n`Is`\n\n, `As`\n\n```\n// Sentinel: a comparable, exported value callers can test for.\nvar (\n    ErrNotFound   = errors.New(\"not found\")\n    ErrRateLimit  = errors.New(\"rate limited\")\n)\n\n// Custom type: when the caller needs structured detail.\ntype ToolError struct {\n    Tool string\n    Code int\n    Err  error\n}\n\nfunc (e *ToolError) Error() string { return fmt.Sprintf(\"tool %s: %v\", e.Tool, e.Err) }\nfunc (e *ToolError) Unwrap() error { return e.Err }        // makes errors.Is see through it\n\n// Callers:\nif errors.Is(err, ErrNotFound) {                            // ✅ works through any wrapping\n    return http.StatusNotFound, nil\n}\n\nvar toolErr *ToolError\nif errors.As(err, &toolErr) {                               // ✅ extract the typed error\n    metrics.ToolFailures.WithLabelValues(toolErr.Tool).Inc()\n}\n\nif err == ErrNotFound { }                                   // ❌ breaks the moment someone wraps\n```\n\n`errors.Is`\n\nfor **identity**, `errors.As`\n\nfor **structure**. Never compare error strings.\n\n```\n// ✅ Handle immediately; the happy path stays at the left margin.\nresp, err := c.Complete(ctx, prompt)\nif err != nil {\n    return fmt.Errorf(\"agent.Run: %w\", err)\n}\nuse(resp)\n// ✅ Retry only what's retryable.\nfor attempt := range maxAttempts {\n    out, err = call(ctx)\n    if err == nil { break }\n    if !errors.Is(err, ErrRateLimit) && !isTransient(err) {\n        return fmt.Errorf(\"agent.call: %w\", err)     // permanent → stop immediately\n    }\n    select {\n    case <-time.After(backoff(attempt)):\n    case <-ctx.Done():\n        return ctx.Err()\n    }\n}\n// ✅ Deliberately ignoring an error is written, not implied.\n_ = resp.Body.Close()\ndefer func() { _ = tx.Rollback() }()   // rollback after a commit is a no-op\njs\n// ✅ Collect failures across a batch instead of stopping at the first.\nvar errs []error\nfor _, chunk := range chunks {\n    if err := index(ctx, chunk); err != nil {\n        errs = append(errs, fmt.Errorf(\"chunk %s: %w\", chunk.ID, err))\n    }\n}\nreturn errors.Join(errs...)     // nil if the slice is empty\n```\n\n`panic`\n\nunwinds the goroutine and crashes the process unless recovered. It is **not** an exception system.\n\n**Panic only when the program cannot sensibly continue:** an impossible invariant, a programming bug, or failed initialization at startup (`regexp.MustCompile`\n\n, `template.Must`\n\n— the `Must`\n\nprefix is the convention).\n\n**Recover only at a process boundary** — one bad request must not kill the server:\n\n```\nfunc Recoverer(next http.Handler) http.Handler {\n    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n        defer func() {\n            if rec := recover(); rec != nil {\n                slog.Error(\"panic in handler\",\n                    \"err\", rec, \"path\", r.URL.Path, \"stack\", string(debug.Stack()))\n                http.Error(w, \"internal error\", http.StatusInternalServerError)\n            }\n        }()\n        next.ServeHTTP(w, r)\n    })\n}\n```\n\n⚠️ ** recover only works in the same goroutine.** A panic inside\n\n`go func(){…}()`\n\nkills the whole process no matter what your HTTP middleware does — every goroutine you spawn needs its own recover, or must be provably panic-free.| Python | Go |\n|---|---|\n`raise ValueError(\"bad temp\")` |\n`return fmt.Errorf(\"bad temperature %v\", t)` |\n`except ValueError:` |\n`if errors.Is(err, ErrBadTemp)` |\n`except SomeError as e: e.field` |\n`var e *SomeError; errors.As(err, &e)` |\n`raise X from err` |\n`fmt.Errorf(\"context: %w\", err)` |\n`finally:` |\n`defer` |\n`except Exception: pass` |\n`_ = f()` (and a comment saying why) |\n| Traceback | The wrap chain you built by hand |\n`sys.exit(1)` on fatal config |\n`log.Fatal` / `panic` in `main` only |\n\n🎯 Actionable rules\n\n- Wrap with\n`%w`\n\nand a`pkg.Func:`\n\nprefix at every layer; log once, at the top.`errors.Is`\n\nfor sentinels,`errors.As`\n\nfor typed detail — never string comparison.- Panic only for programmer bugs and startup failures; recover only at boundaries.\n- Every goroutine you start needs its own panic protection.\n\nGo's headline feature. It is also where every serious Go bug lives.\n\n```\ngo doWork()                      // that's the entire syntax\ngo func(id string) { … }(docID)  // pass arguments explicitly\n```\n\nA goroutine is a **user-space thread multiplexed onto OS threads by the Go runtime**: ~2 KB of initial stack (grown on demand), microsecond creation. A hundred thousand of them in one process is normal; a hundred thousand OS threads is not.\n\n**The rule that prevents most production incidents: never start a goroutine without knowing how it stops.** Every goroutine needs an exit condition — a closed channel, a cancelled context, or a finite loop. A goroutine blocked forever on a channel nobody writes to is a leak: its stack, its captured variables, and everything they reference stay alive until the process dies.\n\n```\n// ❌ leaks one goroutine per request, forever, if nobody reads results\ngo func() { results <- expensive() }()\n\n// ✅ it can always exit\ngo func() {\n    select {\n    case results <- expensive():\n    case <-ctx.Done():\n    }\n}()\n```\n\nA channel is a typed, concurrency-safe queue. Unbuffered channels are a **rendezvous**: the sender blocks until a receiver takes the value.\n\n```\nch := make(chan Token)             // unbuffered: synchronous handoff\nbuf := make(chan Job, 100)         // buffered: sender proceeds until full\nch <- tok                          // send\ntok := <-ch                        // receive\ntok, ok := <-ch                    // ok == false when the channel is closed AND drained\nclose(ch)                          // only the SENDER closes, and only once\nfor tok := range ch { … }          // receives until closed\n```\n\nDirectional types document intent and are checked by the compiler:\n\n```\nfunc produce(out chan<- Token)  { … }   // send-only\nfunc consume(in  <-chan Token)  { … }   // receive-only\n```\n\n| Operation | On a nil channel | On a closed channel |\n|---|---|---|\n| Send | blocks forever | panics |\n| Receive | blocks forever | returns zero value immediately, `ok=false`\n|\n| Close | panics | panics |\n\nConsequences: only ever close from the single owning sender; closing signals \"no more values\", not \"stop\". To stop a consumer, cancel its context.\n\n`select`\n\n```\nselect {\ncase tok := <-tokens:\n    emit(tok)\ncase err := <-errs:\n    return err\ncase <-ctx.Done():                       // cancellation, always include it\n    return ctx.Err()\ncase <-time.After(5 * time.Second):      // per-iteration timeout\n    return errors.New(\"stream stalled\")\ndefault:                                 // non-blocking: runs if nothing else is ready\n    metrics.Idle.Inc()\n}\n```\n\n`select`\n\nblocks until one case is ready, choosing randomly among ready cases. With `default`\n\nit never blocks. ⚠️ `time.After`\n\nallocates a timer per call — inside a hot loop use a reusable `time.NewTimer`\n\n/`Ticker`\n\nand stop it.\n\n**1. Bounded worker pool** — N workers over a job channel. The default for embedding, indexing, or crawling:\n\n```\nfunc EmbedAll(ctx context.Context, chunks []string, workers int) ([][]float32, error) {\n    type result struct {\n        i   int\n        vec []float32\n        err error\n    }\n    jobs := make(chan int)\n    out := make(chan result, len(chunks))\n\n    var wg sync.WaitGroup\n    for range workers {                     // fixed number of goroutines\n        wg.Add(1)\n        go func() {\n            defer wg.Done()\n            for i := range jobs {           // exits when jobs is closed\n                v, err := embed(ctx, chunks[i])\n                out <- result{i, v, err}\n            }\n        }()\n    }\n\n    go func() {                             // feed, then close so workers exit\n        defer close(jobs)\n        for i := range chunks {\n            select {\n            case jobs <- i:\n            case <-ctx.Done():\n                return\n            }\n        }\n    }()\n\n    wg.Wait()\n    close(out)\n\n    vecs := make([][]float32, len(chunks))\n    for r := range out {\n        if r.err != nil {\n            return nil, fmt.Errorf(\"embed chunk %d: %w\", r.i, r.err)\n        }\n        vecs[r.i] = r.vec                   // index carries the order back\n    }\n    return vecs, nil\n}\n```\n\n**2. errgroup** — the concise version when you just need \"run these, stop on first error\":\n\n```\nimport \"golang.org/x/sync/errgroup\"\n\ng, ctx := errgroup.WithContext(ctx)         // ctx is cancelled as soon as one task fails\ng.SetLimit(8)                               // ← bounded concurrency, one line\n\nresults := make([]Doc, len(ids))\nfor i, id := range ids {\n    g.Go(func() error {                     // Go 1.22+: no `i := i` needed\n        d, err := fetch(ctx, id)\n        if err != nil {\n            return fmt.Errorf(\"fetch %s: %w\", id, err)\n        }\n        results[i] = d                      // ✅ distinct indices — no mutex required\n        return nil\n    })\n}\nif err := g.Wait(); err != nil {\n    return nil, err\n}\n```\n\nThis is Go's `asyncio.gather`\n\n+ `Semaphore`\n\n, with cancellation included.\n\n**3. Pipeline / fan-in** — merge several streams into one, the shape behind multi-model or multi-tool streaming:\n\n```\nfunc merge[T any](ctx context.Context, chans ...<-chan T) <-chan T {\n    out := make(chan T)\n    var wg sync.WaitGroup\n    for _, c := range chans {\n        wg.Add(1)\n        go func(c <-chan T) {\n            defer wg.Done()\n            for v := range c {\n                select {\n                case out <- v:\n                case <-ctx.Done():\n                    return\n                }\n            }\n        }(c)\n    }\n    go func() { wg.Wait(); close(out) }()    // close exactly once, after all senders finish\n    return out\n}\n```\n\n`context`\n\n: cancellation that actually propagates\n`context.Context`\n\ncarries a **deadline, a cancellation signal, and request-scoped values** down the call tree. Every function that does I/O takes one as its first parameter.\n\n```\nctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)\ndefer cancel()                            // ✅ ALWAYS defer cancel — otherwise the timer leaks\n\nresp, err := agent.Run(ctx, prompt)\nswitch {\ncase errors.Is(err, context.DeadlineExceeded):\n    http.Error(w, \"upstream timeout\", http.StatusGatewayTimeout)\ncase errors.Is(err, context.Canceled):\n    return                                // client hung up; nothing to write\n}\n```\n\nWhy it matters for AI services: when a user closes the browser mid-stream, `r.Context()`\n\nis cancelled, and that cancellation flows into your model call, your DB query, and every worker goroutine — so you stop paying for tokens nobody will read.\n\n```\n// Values: request-scoped metadata only, with an unexported key type.\ntype ctxKey struct{}\nvar tenantKey ctxKey\n\nctx = context.WithValue(ctx, tenantKey, tenant)\ntenant, ok := ctx.Value(tenantKey).(string)\n```\n\nRules: `ctx`\n\nis the first parameter, never stored in a struct; `context.Background()`\n\nonly in `main`\n\n/tests; never pass `nil`\n\n; values are for tracing/tenancy, never for optional arguments.\n\n`sync`\n\n: when channels are overkill\n\"Don't communicate by sharing memory; share memory by communicating.\" …but a mutex around a cache is simpler than a channel, and simpler wins.\n\n```\ntype Cache struct {\n    mu sync.RWMutex                     // zero value is ready — no initialization\n    m  map[string][]float32\n}\nfunc (c *Cache) Get(k string) ([]float32, bool) {\n    c.mu.RLock()                        // many concurrent readers\n    defer c.mu.RUnlock()\n    v, ok := c.m[k]\n    return v, ok\n}\nfunc (c *Cache) Put(k string, v []float32) {\n    c.mu.Lock()                         // one writer, excludes readers\n    defer c.mu.Unlock()\n    c.m[k] = v\n}\n\nvar once sync.Once\nonce.Do(func() { tokenizer = loadTokenizer() })      // exactly-once init\n\nvar wg sync.WaitGroup                    // wg.Add before `go`, wg.Done in a defer\nvar inflight atomic.Int64                // lock-free counters\ninflight.Add(1); defer inflight.Add(-1)\n```\n\nUse `sync.Map`\n\nonly for its two documented patterns (write-once/read-many, or disjoint key sets per goroutine); otherwise a plain map with an `RWMutex`\n\nis faster and clearer. Put the mutex next to the data it protects, and document what it guards.\n\n```\ngo test -race ./...\ngo run -race ./cmd/api\n```\n\nIt catches unsynchronized concurrent access at runtime (~10× slower, more memory — fine for CI). A data race in Go is undefined behaviour, not just a wrong number: a torn map write crashes the process.\n\n| Symptom | Cause | Fix |\n|---|---|---|\n| Memory grows forever | Goroutine leak — blocked send/receive | Add `<-ctx.Done()` to every `select` ; close channels |\n`all goroutines are asleep - deadlock!` |\nUnbuffered send with no receiver; `wg.Wait()` before `Done`\n|\nCheck ownership; `wg.Add` before `go`\n|\n`send on closed channel` panic |\nMultiple senders, or closing to signal \"stop\" | Only the sole sender closes; cancel via context |\n| Results in the wrong order | Concurrency doesn't preserve order | Carry an index, or write into a preallocated slice |\n| Rare corrupt data | Data race |\n`-race` , then a mutex or channel |\n| 429s / OOM under load | Unbounded fan-out |\n`g.SetLimit(n)` or a worker pool |\n`context deadline exceeded` everywhere |\nOne deadline shared by N sequential calls | Give each call its own budget |\n\n🎯 Actionable rules\n\n- Every goroutine has a known exit path; every blocking\n`select`\n\nhas`<-ctx.Done()`\n\n.- Bound concurrency explicitly —\n`errgroup.SetLimit`\n\nor a fixed worker pool. Never`go`\n\nin an unbounded loop.`ctx`\n\nfirst parameter,`defer cancel()`\n\nalways.- Run\n`-race`\n\nin CI, permanently.\n\nYou don't have to know this to write Go. You do have to know it to explain a p99 latency spike.\n\n```\nG = goroutine   M = OS thread   P = processor (a scheduling context, GOMAXPROCS of them)\n\n   [P0]──local run queue──> G G G        each P owns a queue of runnable Gs\n   [P1]──local run queue──> G            an idle P steals work from a busy one\n     ↑ bound to an M (thread) while running\n   [global run queue] ── overflow ──\n```\n\n`GOMAXPROCS`\n\n`go.uber.org/automaxprocs`\n\n) or your 500m-CPU pod will spawn 64 Ps and thrash.`async`\n\n/`await`\n\ncolouring.**Versus Python:** `asyncio`\n\ngives you one thread cooperatively multiplexing coroutines, and any blocking call freezes all of them. Go gives you preemptive scheduling across every core with no code-colour distinction. That's the core reason a Go gateway holds 50k streaming connections on hardware where a Python one needs process fan-out.\n\nGo's GC is a **concurrent, tri-colour mark-and-sweep** collector, non-generational and non-compacting. It's tuned for **latency, not throughput**: sub-millisecond stop-the-world pauses, at the cost of some CPU and headroom.\n\n```\nGOGC=100      # default: collect when the heap doubles since the last GC\nGOGC=200      # collect half as often — more RAM, less CPU\nGOMEMLIMIT=6GiB   # soft memory ceiling (Go 1.19+) — the setting for containers\nGODEBUG=gctrace=1 ./api    # one line per GC cycle: heap size, pause, CPU share\n```\n\n**In containers, set GOMEMLIMIT to ~80% of the pod's memory limit.** Without it, Go sizes the heap from\n\n`GOGC`\n\nalone, happily grows past the cgroup limit, and gets OOM-killed with no Go-level error. With it, the GC works harder as you approach the ceiling instead of dying.Pointer-heavy structures make GC scan more. Fewer, larger allocations of pointer-free data (`[]float32`\n\nfor embeddings, not `[]*float32`\n\n) is the single biggest GC win in AI workloads.\n\nA write in one goroutine is only guaranteed visible to another if they synchronize — via a channel operation, a mutex, `sync/atomic`\n\n, `sync.Once`\n\n, or `WaitGroup`\n\n. Without that, the compiler and CPU may reorder freely, and the race detector will (eventually) tell you. There is no \"volatile\"; there is `sync/atomic`\n\n.\n\nThe compiler puts values on the **stack** (free, no GC) unless they can outlive the function, in which case they **escape to the heap**.\n\n```\ngo build -gcflags='-m' ./...      # prints \"escapes to heap\" / \"does not escape\"\n```\n\nCommon causes of escape: returning a pointer to a local, storing in an interface, closing over a variable, sending on a channel, `fmt.Sprintf`\n\n.\n\nAllocation-reduction techniques, in order of payoff:\n\n```\nout := make([]Doc, 0, len(ids))         // 1. preallocate with capacity — avoids log(n) regrowths\nm := make(map[string]int, 1000)\n\nvar b strings.Builder                    // 2. builders instead of += concatenation\nb.Grow(estimate)\n\nvar bufPool = sync.Pool{                 // 3. pool big, short-lived buffers on hot paths\n    New: func() any { return new(bytes.Buffer) },\n}\nbuf := bufPool.Get().(*bytes.Buffer)\ndefer func() { buf.Reset(); bufPool.Put(buf) }()\n\nfunc (s *Scanner) Fill(dst []byte) int   // 4. let the caller own the buffer\n```\n\nDo these where a profile says they matter (§12), not everywhere. `sync.Pool`\n\nused carelessly is a memory leak with extra steps.\n\n| Workload | Winner | Why |\n|---|---|---|\n| 20k concurrent SSE streams |\nGo, decisively |\n2 KB goroutines vs event-loop + process fan-out |\n| Fan-out to 50 tools/APIs per request | Go |\n`errgroup` + real parallelism |\n| JSON/protobuf transformation at volume | Go |\nCompiled, GC-friendly, no interpreter overhead |\n| Token/rate accounting, queues, schedulers | Go |\nPredictable latency, cheap primitives |\n| Embedding, training, fine-tuning | Python |\ntorch/numpy/CUDA live there |\n| Data science, notebooks, evaluation | Python |\nThe ecosystem is the product |\n| Model-specific pre/post-processing | Python |\nTokenizers and libraries exist already |\n\n🎯 Actionable rules\n\n- In containers: set\n`GOMEMLIMIT`\n\n(~80% of the limit) and make`GOMAXPROCS`\n\ncgroup-aware.- Preallocate slices and maps whose size you know.\n- Prefer pointer-free bulk data (\n`[]float32`\n\n) to reduce GC scan time.- Optimize allocations only where a pprof profile points.\n\nGo's stdlib is unusually complete: an HTTP/2 server, JSON, TLS, templating, profiling, and testing all ship with the compiler. The list below is what an AI service actually uses.\n\n`net/http`\n\n— the server\n\n```\nmux := http.NewServeMux()\nmux.HandleFunc(\"POST /v1/query\", h.Query)          // Go 1.22+: method + wildcards\nmux.HandleFunc(\"GET /v1/jobs/{id}\", h.GetJob)      // r.PathValue(\"id\")\n\nsrv := &http.Server{\n    Addr:              \":8080\",\n    Handler:           Recoverer(RequestID(Logging(mux))),   // middleware = wrapped handlers\n    ReadHeaderTimeout: 5 * time.Second,     // ✅ blocks Slowloris; the one people forget\n    ReadTimeout:       30 * time.Second,\n    WriteTimeout:      0,                   // 0 for SSE/streaming endpoints; set it otherwise\n    IdleTimeout:       120 * time.Second,\n    MaxHeaderBytes:    1 << 20,\n}\n\n// Graceful shutdown: stop accepting, let in-flight requests finish.\ngo func() {\n    if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {\n        slog.Error(\"listen\", \"err\", err); os.Exit(1)\n    }\n}()\n\nctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)\ndefer stop()\n<-ctx.Done()\nshutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)\ndefer cancel()\n_ = srv.Shutdown(shutdownCtx)\n```\n\n`chi`\n\nadds routers, groups, and middleware chains on top of `http.Handler`\n\nwithout inventing a new handler type — which is why it composes with everything (and why this repo uses it).\n\n`net/http`\n\n— the client\n\n``` js\nvar client = &http.Client{                 // ✅ ONE client for the process, reused\n    Timeout: 60 * time.Second,             // total budget, including body read\n    Transport: &http.Transport{\n        MaxIdleConns:        200,\n        MaxIdleConnsPerHost: 100,          // default is 2 — far too low for an LLM proxy\n        IdleConnTimeout:     90 * time.Second,\n    },\n}\n\nreq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))\nif err != nil { return fmt.Errorf(\"llm.Complete: %w\", err) }\nreq.Header.Set(\"Content-Type\", \"application/json\")\n\nresp, err := client.Do(req)\nif err != nil { return fmt.Errorf(\"llm.Complete: %w\", err) }\ndefer resp.Body.Close()                    // ✅ ALWAYS — otherwise the connection leaks\nif resp.StatusCode != http.StatusOK {\n    b, _ := io.ReadAll(io.LimitReader(resp.Body, 4<<10))    // cap what you read on errors\n    return fmt.Errorf(\"llm.Complete: status %d: %s\", resp.StatusCode, b)\n}\n```\n\nThree non-negotiables: reuse the client, always close the body, always pass a context. Creating an `http.Client`\n\nper request disables connection pooling and exhausts sockets under load.\n\n`encoding/json`\n\n```\ntype QueryIn struct {\n    Query       string   `json:\"query\"`\n    Temperature float64  `json:\"temperature,omitempty\"`   // omit when zero\n    Tools       []string `json:\"tools,omitempty\"`\n    internal    string   `json:\"-\"`                       // never marshalled\n}\n\nb, err := json.Marshal(v)\nerr = json.Unmarshal(b, &v)                               // note the pointer\n\ndec := json.NewDecoder(r.Body)                            // ✅ stream, don't ReadAll\ndec.DisallowUnknownFields()                               // ✅ typo'd client fields become errors\nif err := dec.Decode(&in); err != nil {\n    http.Error(w, \"invalid body\", http.StatusBadRequest); return\n}\n\nvar raw json.RawMessage                                    // defer parsing tool args\nenc := json.NewEncoder(w); enc.Encode(out)                 // stream the response out\n```\n\n⚠️ Only **exported** fields are marshalled. ⚠️ Unmarshalling into `map[string]any`\n\nturns every number into `float64`\n\n— decode into a struct whenever you can. For hot paths, `json.Decoder`\n\non the body avoids materializing the whole payload.\n\nCustom marshalling for domain types:\n\n```\nfunc (r Role) MarshalJSON() ([]byte, error) { return json.Marshal(string(r)) }\n```\n\n`log/slog`\n\n— structured logging (Go 1.21+)\n\n```\nlogger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))\nslog.SetDefault(logger)\n\nslog.Info(\"tool completed\", \"tool\", name, \"ms\", elapsed.Milliseconds(), \"tokens\", n)\nslog.Error(\"model call failed\", \"err\", err, \"model\", cfg.Model, \"attempt\", i)\n\nreqLog := logger.With(\"request_id\", rid, \"tenant\", tenant)   // bind once, reuse per request\nreqLog.Info(\"received\")\n```\n\nStructured key-value output is what makes logs queryable in Loki/Datadog. Never log prompts, keys, or full request bodies — log ids, counts, durations, and truncated previews.\n\n`time`\n\n```\ntime.Now(); time.Since(start)                    // monotonic for durations\n30 * time.Second; 500 * time.Millisecond         // Durations are typed ints — no unit bugs\nt.Format(time.RFC3339); time.Parse(time.RFC3339, s)\ntime.Now().UTC()                                 // store UTC, convert at the edge\n\ntick := time.NewTicker(10 * time.Second)\ndefer tick.Stop()                                // ✅ tickers leak if not stopped\nselect {\ncase <-tick.C: flushMetrics()\ncase <-ctx.Done(): return\n}\n```\n\n`io`\n\nand `bufio`\n\n— the composable plumbing\n\n```\nio.Copy(dst, src)                                  // stream, constant memory\nio.ReadAll(io.LimitReader(r, 10<<20))              // ✅ always cap untrusted input\nio.MultiWriter(w, &buf)                            // tee the response into a buffer\n\nsc := bufio.NewScanner(resp.Body)                  // line-by-line: perfect for SSE\nsc.Buffer(make([]byte, 0, 64*1024), 1<<20)         // ✅ raise the 64 KB line limit\nfor sc.Scan() {\n    line := sc.Text()\n    …\n}\nif err := sc.Err(); err != nil { … }               // ✅ Scan() returning false isn't always EOF\n```\n\n| Package | Use it for |\n|---|---|\n`context` |\nCancellation and deadlines (§6.5) |\n`sync` / `sync/atomic`\n|\nMutexes, `WaitGroup` , `Once` , counters (§6.6) |\n`errors` |\n`Is` , `As` , `Join` , `Unwrap` (§5) |\n`strconv` / `strings` / `bytes`\n|\nConversion and text handling (§2.4) |\n`regexp` |\nRE2 — linear time, no catastrophic backtracking; `MustCompile` at package level |\n`os` / `os/signal`\n|\nEnv, files, SIGTERM handling |\n`flag` |\nSmall CLIs; use `cobra` for a command tree |\n`embed` |\n`//go:embed prompts/*.md` — bake prompts and migrations into the binary |\n`text/template` |\nPrompt templating with named fields |\n`database/sql` (+ `sqlx` , `pgx` ) |\nSQL; always `QueryContext` , always `defer rows.Close()` , always check `rows.Err()`\n|\n`encoding/base64` , `crypto/*`\n|\nTokens, signatures, `crypto/rand` for secrets |\n`net/http/httptest` |\nIn-process HTTP tests (§10) |\n`runtime/pprof` , `net/http/pprof`\n|\nProfiling (§12) |\n`testing` |\nTests, benchmarks, fuzzing — all built in |\n\nThird-party worth adopting: `golang.org/x/sync/errgroup`\n\nand `singleflight`\n\n, `go-chi/chi`\n\n, `jmoiron/sqlx`\n\n, `stretchr/testify/require`\n\n, `pressly/goose`\n\n, `golang.org/x/time/rate`\n\n, and OpenTelemetry for traces. Go culture keeps dependency trees small — prefer the stdlib until it genuinely hurts.\n\n🎯 Actionable rules\n\n- One\n`http.Client`\n\nper process with a timeout and a tuned transport;`defer resp.Body.Close()`\n\nalways.- Explicit\n`http.Server`\n\ntimeouts and graceful shutdown on SIGTERM.`json.Decoder`\n\n+`DisallowUnknownFields`\n\non request bodies;`io.LimitReader`\n\non anything untrusted.`slog`\n\nwith key-value pairs from day one — retrofitting structure is miserable.\n\nWhat Go is actually for in an AI stack: the request path, the fan-out, and the streaming.\n\n```\nfunc (c *LLM) Stream(ctx context.Context, prompt string, out chan<- string) error {\n    req, _ := http.NewRequestWithContext(ctx, http.MethodPost, c.url, encode(prompt))\n    req.Header.Set(\"Accept\", \"text/event-stream\")\n\n    resp, err := c.http.Do(req)\n    if err != nil {\n        return fmt.Errorf(\"llm.Stream: %w\", err)\n    }\n    defer resp.Body.Close()\n\n    sc := bufio.NewScanner(resp.Body)\n    sc.Buffer(make([]byte, 0, 64*1024), 1<<20)      // model chunks exceed the 64 KB default\n    for sc.Scan() {\n        line, ok := strings.CutPrefix(sc.Text(), \"data: \")\n        if !ok || line == \"\" {\n            continue\n        }\n        if line == \"[DONE]\" {\n            return nil\n        }\n        var ev struct {\n            Delta struct{ Text string } `json:\"delta\"`\n        }\n        if err := json.Unmarshal([]byte(line), &ev); err != nil {\n            return fmt.Errorf(\"llm.Stream: decode %q: %w\", truncate(line, 80), err)\n        }\n        select {\n        case out <- ev.Delta.Text:\n        case <-ctx.Done():                          // client disconnected: stop paying for tokens\n            return ctx.Err()\n        }\n    }\n    return sc.Err()\n}\nfunc (h *Handler) Stream(w http.ResponseWriter, r *http.Request) {\n    rc := http.NewResponseController(w)             // Go 1.20+; replaces the http.Flusher cast\n    w.Header().Set(\"Content-Type\", \"text/event-stream\")\n    w.Header().Set(\"Cache-Control\", \"no-cache\")\n    w.Header().Set(\"X-Accel-Buffering\", \"no\")       // stop nginx from buffering your stream\n\n    ctx := r.Context()                              // cancelled when the client goes away\n    tokens := make(chan string, 16)\n    errc := make(chan error, 1)\n    go func() { errc <- h.llm.Stream(ctx, r.FormValue(\"q\"), tokens); close(tokens) }()\n\n    for {\n        select {\n        case tok, ok := <-tokens:\n            if !ok {\n                fmt.Fprint(w, \"data: [DONE]\\n\\n\")\n                _ = rc.Flush()\n                return\n            }\n            fmt.Fprintf(w, \"data: %s\\n\\n\", tok)\n            _ = rc.Flush()                          // ✅ without Flush nothing reaches the client\n        case <-ctx.Done():\n            return\n        case <-time.After(30 * time.Second):\n            slog.Warn(\"stream stalled\", \"path\", r.URL.Path)\n            return\n        }\n    }\n}\n```\n\nRemember to set `WriteTimeout: 0`\n\non the server for streaming routes (§8.1), or the connection dies mid-answer.\n\n```\ntype Tool struct {\n    Name        string          `json:\"name\"`\n    Description string          `json:\"description\"`\n    Schema      json.RawMessage `json:\"input_schema\"`     // sent verbatim to the model\n    Run         func(ctx context.Context, args json.RawMessage) (string, error) `json:\"-\"`\n}\n\ntype Registry struct {\n    mu    sync.RWMutex\n    tools map[string]Tool\n}\n\nfunc (r *Registry) Register(t Tool) error {\n    r.mu.Lock(); defer r.mu.Unlock()\n    if _, dup := r.tools[t.Name]; dup {\n        return fmt.Errorf(\"registry.Register: duplicate tool %q\", t.Name)\n    }\n    r.tools[t.Name] = t\n    return nil\n}\n\nfunc (r *Registry) Dispatch(ctx context.Context, name string, args json.RawMessage) (string, error) {\n    r.mu.RLock(); t, ok := r.tools[name]; r.mu.RUnlock()\n    if !ok {\n        return \"\", fmt.Errorf(\"registry.Dispatch: unknown tool %q\", name)   // never trust the model\n    }\n    ctx, cancel := context.WithTimeout(ctx, 30*time.Second)                // ✅ per-tool budget\n    defer cancel()\n    return t.Run(ctx, args)\n}\n```\n\nTwo things the model must never control: which tools exist, and how long they may run.\n\n```\nimport \"golang.org/x/time/rate\"\n\ntype Client struct {\n    http    *http.Client\n    limiter *rate.Limiter          // rate.NewLimiter(rate.Limit(50), 100) → 50 rps, burst 100\n    sem     chan struct{}          // concurrency cap: make(chan struct{}, 16)\n}\n\nfunc (c *Client) Complete(ctx context.Context, prompt string) (string, error) {\n    if err := c.limiter.Wait(ctx); err != nil {          // blocks or returns on cancellation\n        return \"\", fmt.Errorf(\"llm.Complete: rate wait: %w\", err)\n    }\n    select {                                             // bound in-flight requests\n    case c.sem <- struct{}{}:\n        defer func() { <-c.sem }()\n    case <-ctx.Done():\n        return \"\", ctx.Err()\n    }\n\n    var lastErr error\n    for attempt := range 4 {\n        out, err := c.do(ctx, prompt)\n        if err == nil {\n            return out, nil\n        }\n        lastErr = err\n        var re *RetryableError\n        if !errors.As(err, &re) {\n            return \"\", fmt.Errorf(\"llm.Complete: %w\", err)          // permanent → stop\n        }\n        delay := re.RetryAfter                                       // honour the server's hint\n        if delay == 0 {\n            delay = time.Duration(1<<attempt) * 200 * time.Millisecond\n        }\n        jitter := time.Duration(rand.Int64N(int64(delay / 2)))       // math/rand/v2\n        select {\n        case <-time.After(delay + jitter):\n        case <-ctx.Done():\n            return \"\", ctx.Err()\n        }\n    }\n    return \"\", fmt.Errorf(\"llm.Complete: exhausted retries: %w\", lastErr)\n}\n// Go owns HTTP, auth, tenancy, and fan-out; Python owns the model work.\nfunc (s *Service) Answer(ctx context.Context, tenant, q string) (Answer, error) {\n    ctx, cancel := context.WithTimeout(ctx, 45*time.Second)\n    defer cancel()\n\n    g, gctx := errgroup.WithContext(ctx)\n    var (\n        docs []Doc\n        vec  []float32\n    )\n    g.Go(func() (err error) { docs, err = s.repo.Search(gctx, tenant, q); return })\n    g.Go(func() (err error) { vec, err = s.python.Embed(gctx, q); return })   // internal REST\n    if err := g.Wait(); err != nil {\n        return Answer{}, fmt.Errorf(\"service.Answer: %w\", err)\n    }\n    …\n}\n```\n\nRetrieval and embedding run in parallel; either failure cancels the other; the whole request shares one deadline. That is ~15 lines of Go for what needs careful orchestration elsewhere.\n\n`singleflight`\n\n— collapse duplicate work\nWhen 500 users ask the same question in the same second, do the expensive thing once:\n\n``` js\nimport \"golang.org/x/sync/singleflight\"\n\nvar group singleflight.Group\n\nfunc (c *Cache) Embed(ctx context.Context, text string) ([]float32, error) {\n    key := hash(text)\n    if v, ok := c.Get(key); ok {\n        return v, nil\n    }\n    v, err, _ := group.Do(key, func() (any, error) {     // concurrent callers share one result\n        return c.upstream.Embed(ctx, text)\n    })\n    if err != nil {\n        return nil, fmt.Errorf(\"cache.Embed: %w\", err)\n    }\n    return v.([]float32), nil\n}\n```\n\n🎯 Actionable rules\n\n- Propagate\n`r.Context()`\n\ninto every model call so a disconnect stops the spend.- Bound everything: rate limiter, concurrency semaphore, per-tool timeout, retry cap.\n- Flush after every SSE write, and disable proxy buffering.\n- Validate tool names against the registry — the model's output is untrusted input.\n\nTesting is in the standard library, in the same package, with no framework to choose. That's a feature.\n\n```\n// internal/service/summarize_test.go\npackage service\n\nfunc TestSummarize(t *testing.T) {\n    tests := []struct {\n        name     string\n        text     string\n        maxWords int\n        want     string\n        wantErr  bool\n    }{\n        {name: \"truncates\", text: \"a b c d\", maxWords: 2, want: \"a b\"},\n        {name: \"collapses whitespace\", text: \" a   b \", maxWords: 5, want: \"a b\"},\n        {name: \"rejects zero\", text: \"a\", maxWords: 0, wantErr: true},\n    }\n\n    for _, tt := range tests {\n        t.Run(tt.name, func(t *testing.T) {           // a named subtest per case\n            t.Parallel()                              // ✅ subtests run concurrently\n            got, err := Summarize(tt.text, tt.maxWords)\n            if tt.wantErr {\n                require.Error(t, err)\n                return\n            }\n            require.NoError(t, err)\n            require.Equal(t, tt.want, got)\n        })\n    }\n}\n```\n\n`go test`\n\nfailures print the subtest path (`TestSummarize/rejects_zero`\n\n), so you know exactly which case broke. Per this repo's conventions, use `testify/require`\n\n(stops the test) over `assert`\n\n(continues) and over bare `t.Fatal`\n\n.\n\nHelpers that pay for themselves:\n\n```\nt.Helper()                    // in a helper: failures report the CALLER's line\nt.Cleanup(func() { … })       // teardown, LIFO, runs even on failure — better than defer\nt.TempDir()                   // auto-removed temp directory\nt.Setenv(\"MODEL\", \"x\")        // auto-restored env (forbids t.Parallel in that test)\nt.Context()                   // Go 1.24+: a context cancelled at test end\ntesting.Short()               // skip slow tests under `go test -short`\n```\n\nBecause interfaces are structural and defined by the consumer, a fake is just a struct:\n\n```\ntype fakeLLM struct {\n    replies []string\n    calls   int\n}\n\nfunc (f *fakeLLM) Complete(ctx context.Context, prompt string) (string, error) {\n    if f.calls >= len(f.replies) {\n        return \"\", errors.New(\"fakeLLM: out of replies\")\n    }\n    f.calls++\n    return f.replies[f.calls-1], nil\n}\n\nfunc TestAgentUsesCalculator(t *testing.T) {\n    llm := &fakeLLM{replies: []string{`{\"tool\":\"calculator\",\"args\":{\"expression\":\"10*5\"}}`}}\n    agent, err := NewAgent(AgentConfig{Name: \"t\"}, llm)\n    require.NoError(t, err)\n\n    resp, err := agent.Run(t.Context(), \"calculate 10 * 5\")\n    require.NoError(t, err)\n    require.Equal(t, StatusOK, resp.Status)\n    require.Equal(t, 1, llm.calls)\n}\n```\n\nNo mocking library, no code generation, no patching. If faking your interface is painful, the interface is too big.\n\n`httptest`\n\n```\n// Test a handler without a network.\nfunc TestQueryHandler(t *testing.T) {\n    h := NewHandler(&fakeService{})\n    req := httptest.NewRequest(http.MethodPost, \"/v1/query\", strings.NewReader(`{\"query\":\"hi\"}`))\n    rec := httptest.NewRecorder()\n\n    h.Query(rec, req)\n\n    require.Equal(t, http.StatusOK, rec.Code)\n    require.JSONEq(t, `{\"answer\":\"hi!\"}`, rec.Body.String())\n}\n\n// Stub an upstream provider with a real server.\nfunc TestClientRetriesOn429(t *testing.T) {\n    var hits atomic.Int32\n    srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n        if hits.Add(1) < 3 {\n            w.WriteHeader(http.StatusTooManyRequests)\n            return\n        }\n        _, _ = io.WriteString(w, `{\"content\":\"ok\"}`)\n    }))\n    defer srv.Close()\n\n    c := NewClient(srv.URL)\n    out, err := c.Complete(t.Context(), \"hi\")\n    require.NoError(t, err)\n    require.Equal(t, \"ok\", out)\n    require.EqualValues(t, 3, hits.Load())\n}\njs\nvar update = flag.Bool(\"update\", false, \"update golden files\")\n\nfunc TestPromptRendering(t *testing.T) {\n    got := RenderSystemPrompt(cfg)\n    golden := filepath.Join(\"testdata\", \"system_prompt.golden\")\n    if *update {\n        require.NoError(t, os.WriteFile(golden, []byte(got), 0o644))\n    }\n    want, err := os.ReadFile(golden)          // testdata/ is ignored by the go tool\n    require.NoError(t, err)\n    require.Equal(t, string(want), got)\n}\n```\n\n`go test ./... -update`\n\nregenerates; the diff shows up in code review. Ideal for prompts, schemas, and serialized payloads.\n\n```\n//go:build integration\n\npackage repo_test\n// … tests that need a real Postgres (testcontainers-go), run with:\n//   go test -tags integration ./...\n```\n\nFast unit tests in pre-commit, tagged integration tests in CI — the split this repo's [ CLAUDE.md](//CLAUDE.md) prescribes.\n\n```\nfunc BenchmarkChunk(b *testing.B) {\n    doc := strings.Repeat(\"word \", 100_000)\n    b.ReportAllocs()\n    b.ResetTimer()\n    for b.Loop() {                 // Go 1.24+; older: for i := 0; i < b.N; i++\n        sink = Chunk(doc, 1000, 200)\n    }\n}\nvar sink []string                  // package-level: stops the compiler optimizing the call away\ngo test -bench=Chunk -benchmem -count=10 ./internal/text | tee new.txt\nbenchstat old.txt new.txt          # statistically meaningful comparison, not one lucky run\n```\n\n`-benchmem`\n\nprints `B/op`\n\nand `allocs/op`\n\n— usually more actionable than ns/op, because allocations drive GC pressure.\n\n```\nfunc FuzzParseToolCall(f *testing.F) {\n    f.Add(`{\"tool\":\"calc\",\"args\":{}}`)                 // seed corpus\n    f.Fuzz(func(t *testing.T, s string) {\n        _, _ = ParseToolCall([]byte(s))                // must never panic on any input\n    })\n}\ngo test -fuzz=FuzzParseToolCall -fuzztime=60s ./internal/agent\n```\n\nAnything that parses model output is a prime fuzz target: LLMs emit truncated JSON, nested fences, and 10 MB of whitespace. Crashes land in `testdata/fuzz/`\n\nand become permanent regression tests.\n\n```\ngo test ./...                       # everything\ngo test -race ./...                 # ✅ what CI must run\ngo test -run TestAgent/calculator    # by name, subtests included\ngo test -short ./...                # skip the slow ones\ngo test -cover ./... && go tool cover -html=cover.out\ngo test -count=1 ./...              # bypass the test cache\n```\n\n🎯 Actionable rules\n\n- Table-driven +\n`t.Run`\n\n+`t.Parallel`\n\nas the default shape.- Hand-written fakes over mock frameworks; keep interfaces small enough to fake.\n`-race`\n\nin CI, always; fuzz anything that parses untrusted or model-generated input.- Benchmark with\n`-benchmem`\n\nand compare with`benchstat`\n\n, never by eyeballing one run.\n\n```\ngo mod init github.com/acme/agent-service\ngo get github.com/go-chi/chi/v5@latest\ngo get -u ./...            # update dependencies\ngo mod tidy                # add what's used, drop what isn't — run before every commit\ngo mod download            # populate the module cache (Docker builds)\ngo mod why github.com/x/y  # who pulled this in?\ngo work init ./backend-go ./shared    # multi-module workspaces\n```\n\n`go.mod`\n\ndeclares the module path, Go version, and dependencies; `go.sum`\n\nholds cryptographic hashes. **Commit both.** There is no venv: the toolchain resolves per-module, and builds are reproducible by construction.\n\nVersioning is [semantic import versioning](https://go.dev/ref/mod): `v2+`\n\nchanges the import path (`.../chi/v5`\n\n). Awkward at first, but it makes two major versions coexist in one build.\n\n```\nbackend-go/\n├── go.mod / go.sum\n├── Makefile\n├── cmd/\n│   └── api/\n│       ├── main.go            # wiring only: config → deps → server\n│       └── routes.go\n├── internal/                  # ← the compiler FORBIDS imports from outside this module\n│   ├── handler/               # HTTP: decode, call service, encode. No business logic.\n│   ├── service/               # business logic. No HTTP types, no SQL.\n│   ├── repo/                  # DB access (sqlx). No business rules.\n│   ├── model/                 # domain types shared across layers\n│   └── middleware/\n├── pkg/                       # only for packages you intend other repos to import\n├── migrations/                # goose SQL files\n└── testdata/                  # golden files, fixtures\n```\n\n`internal/`\n\nis enforced by the compiler`pkg/`\n\nis opt-in publicity.`handler → service → repo`\n\n. If two packages need each other, extract the shared type into `model/`\n\n. Go rejects import cycles at compile time, so bad layering fails the build rather than rotting.`service.Agent`\n\n, not `service.ServiceAgent`\n\n. The package name is part of every call site.`main.go`\n\ndoes wiring and nothing else: read config, construct dependencies, start the server, handle SIGTERM.\n\n```\ngofmt -l .            # formatting is not a debate; gofmt decides\ngo vet ./...          # correctness heuristics: printf verbs, lost cancels, copied locks\ngo build ./...\ngo test -race ./...\ngo run ./cmd/api\ngo generate ./...     # //go:generate directives (mocks, enums, sqlc)\ngovulncheck ./...     # ✅ CVEs in YOUR call paths, not just in go.sum\n```\n\n`golangci-lint`\n\nbundles the linters worth running:\n\n```\n# .golangci.yml\nlinters:\n  enable:\n    - errcheck      # unchecked errors ← the highest-value linter in Go\n    - govet\n    - staticcheck   # the deep one: dead code, misuse, simplifications\n    - revive        # style + doc comments\n    - ineffassign\n    - bodyclose     # unclosed HTTP response bodies\n    - noctx         # HTTP requests built without a context\n    - sqlclosecheck\n    - gosec\nissues:\n  exclude-rules:\n    - path: _test\\.go\n      linters: [gosec, errcheck]\n```\n\nAdd `air`\n\nfor hot reload in development (`make dev-go`\n\nin this repo), and a `Makefile`\n\nso every service has the same verbs: `make dev`\n\n, `make test`\n\n, `make lint`\n\n, `make migrate-up`\n\n.\n\n```\ntype Config struct {\n    DatabaseURL string\n    RedisURL    string\n    Port        int\n    APIKey      string\n}\n\nfunc Load() (Config, error) {\n    c := Config{\n        RedisURL: \"redis://localhost:6379/0\",     // defaults in code\n        Port:     8080,\n    }\n    var missing []string\n    for _, f := range []struct{ key string; dst *string }{\n        {\"DATABASE_URL\", &c.DatabaseURL},\n        {\"ANTHROPIC_API_KEY\", &c.APIKey},\n    } {\n        if *f.dst = os.Getenv(f.key); *f.dst == \"\" {\n            missing = append(missing, f.key)\n        }\n    }\n    if len(missing) > 0 {\n        return Config{}, fmt.Errorf(\"config.Load: missing env: %s\", strings.Join(missing, \", \"))\n    }\n    …\n    return c, nil\n}\n```\n\nValidate everything in `main`\n\nand exit non-zero on failure. A service that refuses to start beats one that fails on request #4000. (`kelseyhightower/envconfig`\n\nor `caarlos0/env`\n\ndo this with struct tags if you prefer.)\n\nGo's single static binary makes this dramatically simpler than the Python equivalent — the final image can contain *only your binary*.\n\n```\n# syntax=docker/dockerfile:1.9\n\n# ────────────────────────── Stage 1: build ──────────────────────────\nFROM golang:1.23-bookworm AS builder\nWORKDIR /src\n\n# Dependencies first: this layer is cached until go.mod/go.sum change.\nCOPY go.mod go.sum ./\nRUN --mount=type=cache,target=/go/pkg/mod go mod download\n\nCOPY . .\n\nARG VERSION=dev\nARG COMMIT=unknown\n# CGO_ENABLED=0 → a fully static binary that runs on scratch/distroless.\n# -trimpath      → no local paths in the binary (reproducible builds).\n# -ldflags \"-s -w\" → strip symbols/DWARF: ~25% smaller.\n# -X             → stamp build metadata into vars for /healthz and logs.\nRUN --mount=type=cache,target=/go/pkg/mod \\\n    --mount=type=cache,target=/root/.cache/go-build \\\n    CGO_ENABLED=0 GOOS=linux go build \\\n      -trimpath \\\n      -ldflags=\"-s -w -X main.version=${VERSION} -X main.commit=${COMMIT}\" \\\n      -o /out/api ./cmd/api\n\n# ────────────────────────── Stage 2: runtime ──────────────────────────\nFROM gcr.io/distroless/static-debian12:nonroot AS runtime\n# distroless/static = CA certs + tzdata + /etc/passwd, no shell, no package manager.\n# Use :nonroot (uid 65532) so the container never runs as root.\n\nCOPY --from=builder /out/api /api\nCOPY --from=builder /src/migrations /migrations     # only if the binary applies them\n\nUSER nonroot:nonroot\nEXPOSE 8080\nENV GOMEMLIMIT=450MiB GOMAXPROCS=2                  # match the pod's limits (see §7.2)\n\nENTRYPOINT [\"/api\"]\n```\n\n**Why each decision:**\n\n| Decision | Reason |\n|---|---|\n`CGO_ENABLED=0` |\nRemoves the libc dependency, so the binary runs on `scratch` /`distroless` . If you need cgo (SQLite, some crypto), build on and ship to a matching glibc base instead. |\ndistroless/static, not `alpine` or `ubuntu`\n|\nNo shell, no package manager, no CVE churn from utilities you never use. Final image ≈ your binary + 2 MB. `scratch` is even smaller but lacks CA certs and tzdata, which any HTTPS client needs. |\n`:nonroot` tag |\nRuns as uid 65532 with no writable filesystem — satisfies `runAsNonRoot` policies out of the box. |\n| Deps before source | Same caching logic as everywhere: `go mod download` is reused until `go.sum` changes. |\n| BuildKit cache mounts | Keeps the module and build caches between builds without baking them into layers. |\n`-trimpath` + `-ldflags=\"-s -w\"`\n|\nReproducible and ~25% smaller; strip only after you've decided you don't need symbols in prod profiles. |\n`-X main.version=…` |\nThe binary can report its own build; invaluable when three replicas disagree. |\nNo `HEALTHCHECK`\n|\nDistroless has no shell or curl. Let Kubernetes do an HTTP probe against `/healthz` ; a Docker-level healthcheck would force you to ship a fatter image. |\n`GOMEMLIMIT` / `GOMAXPROCS`\n|\nThe Go runtime doesn't see cgroup limits before Go 1.25 — set them explicitly to the pod's limits (§7.1–§7.2). |\n`ENTRYPOINT` in exec form |\nYour binary is PID 1 and receives SIGTERM directly — which is exactly what `srv.Shutdown` needs (§8.1). |\n\n```\n# Need HTTPS + timezones on scratch? Copy them in rather than adding a base OS:\n# COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/\nDOCKER_BUILDKIT=1 docker build \\\n  --platform linux/amd64 \\\n  --build-arg VERSION=1.4.2 --build-arg COMMIT=$(git rev-parse --short HEAD) \\\n  -t agent-api:1.4.2 .\n\ndocker run --rm -p 8080:8080 --env-file .env --read-only --cap-drop=ALL agent-api:1.4.2\ndocker images agent-api:1.4.2          # expect ~15–30 MB total\n```\n\n`.dockerignore`\n\n:\n\n```\n.git/\nbin/\ntmp/\n*_test.go\ntestdata/\n.env\nDockerfile\n```\n\n**Pre-ship checklist:** image under ~30 MB · `docker run … --read-only`\n\nworks · SIGTERM drains in-flight requests within the grace period · no secrets in `docker history`\n\n· `govulncheck`\n\nclean · `--platform linux/amd64`\n\nwhen building on Apple silicon for x86 nodes.\n\n🎯 Actionable rules\n\n- Put everything in\n`internal/`\n\nunless another repo must import it.`go mod tidy`\n\n,`gofmt`\n\n,`go vet`\n\n,`golangci-lint`\n\n,`govulncheck`\n\n— all in CI.- Validate config in\n`main`\n\nand exit non-zero on anything missing.- Ship a distroless static binary and set\n`GOMEMLIMIT`\n\n/`GOMAXPROCS`\n\nto the pod's limits.\n\n```\n// .vscode/launch.json\n{\n  \"version\": \"0.2.0\",\n  \"configurations\": [\n    {\n      \"name\": \"Run API\",\n      \"type\": \"go\",\n      \"request\": \"launch\",\n      \"mode\": \"debug\",\n      \"program\": \"${workspaceFolder}/cmd/api\",\n      \"env\": { \"DATABASE_URL\": \"postgres://dev:dev@localhost:5432/app\", \"LOG_LEVEL\": \"debug\" },\n      \"args\": [\"--verbose\"]\n    },\n    {\n      \"name\": \"Debug current test\",\n      \"type\": \"go\",\n      \"request\": \"launch\",\n      \"mode\": \"test\",\n      \"program\": \"${fileDirname}\",\n      \"args\": [\"-test.run\", \"TestAgentUsesCalculator\", \"-test.v\"],\n      \"buildFlags\": \"-race\"\n    },\n    {\n      \"name\": \"Attach to container (dlv)\",\n      \"type\": \"go\",\n      \"request\": \"attach\",\n      \"mode\": \"remote\",\n      \"port\": 2345,\n      \"host\": \"127.0.0.1\",\n      \"substitutePath\": [{ \"from\": \"${workspaceFolder}\", \"to\": \"/src\" }]\n    }\n  ]\n}\n# In the container, for the attach config:\ndlv exec --headless --listen=:2345 --api-version=2 --accept-multiclient /api\n# Build with debug info: go build -gcflags=\"all=-N -l\"   (disables inlining/optimization)\n// .vscode/settings.json\n{\n  \"go.useLanguageServer\": true,\n  \"go.lintTool\": \"golangci-lint\",\n  \"go.lintOnSave\": \"package\",\n  \"go.testFlags\": [\"-race\", \"-count=1\"],\n  \"gopls\": { \"ui.semanticTokens\": true, \"staticcheck\": true }\n}\n```\n\n**Breakpoint techniques that matter:** conditional breakpoints (`docID == \"doc-9182\"`\n\n) to catch iteration 4000 of a loop; logpoints for tracing without a rebuild; and the **Goroutines panel**, which is Go-specific and invaluable — it shows every live goroutine with its stack, so a leak or a deadlock is visible directly. The Debug Console evaluates expressions and lets you change variables to force an error branch.\n\nDelve on the command line, when you're on a server:\n\n```\ndlv debug ./cmd/api\n(dlv) break service.(*Agent).Run\n(dlv) condition 1 prompt == \"calculate 10 * 5\"\n(dlv) continue ; locals ; goroutines ; stack ; print cfg\npython\nimport _ \"net/http/pprof\"       // registers /debug/pprof/* on the DefaultServeMux\n\ngo func() {\n    // ✅ bind to localhost or an admin port — never expose pprof publicly\n    slog.Error(\"pprof\", \"err\", http.ListenAndServe(\"127.0.0.1:6060\", nil))\n}()\ngo tool pprof -http=:8081 http://localhost:6060/debug/pprof/profile?seconds=30   # CPU\ngo tool pprof -http=:8081 http://localhost:6060/debug/pprof/heap                 # memory\ngo tool pprof http://localhost:6060/debug/pprof/allocs                           # all allocations\ncurl \"http://localhost:6060/debug/pprof/goroutine?debug=2\"   # every goroutine's stack ← leaks\ncurl \"http://localhost:6060/debug/pprof/block\"               # blocking (needs SetBlockProfileRate)\ncurl \"http://localhost:6060/debug/pprof/mutex\"               # contention (needs SetMutexProfileFraction)\n```\n\n`-http=:8081`\n\nopens an interactive flame graph in the browser. The workflow for the three problems you'll actually hit:\n\n| Symptom | Profile | What you're looking for |\n|---|---|---|\n| High CPU | `profile?seconds=30` |\nThe widest frame in the flame graph |\n| Memory grows without bound |\n`heap` + `goroutine?debug=2`\n|\nA goroutine count that only rises = a leak |\n| Latency spikes at steady CPU |\n`block` , `mutex` , `gctrace=1`\n|\nLock contention or GC pressure |\n\nThe **execution tracer** shows scheduling, GC, and syscalls on a timeline:\n\n```\ncurl -o trace.out \"http://localhost:6060/debug/pprof/trace?seconds=5\"\ngo tool trace trace.out\nGODEBUG=gctrace=1 ./api            # one line per GC: heap, pause, CPU share\nGODEBUG=schedtrace=1000 ./api      # scheduler state every second\nGODEBUG=inittrace=1 ./api          # slow package init\nGOTRACEBACK=all ./api              # dump ALL goroutine stacks on a fatal panic\ngo build -gcflags='-m' ./...       # escape analysis decisions\ngo test -race ./...                # data races\ngo tool nm -size bin/api | sort -k2 -n | tail   # what's making the binary big\n```\n\n`kill -QUIT <pid>`\n\non a hung Go process dumps every goroutine's stack to stderr — the Go equivalent of `py-spy dump`\n\n, and it's built in.\n\n🎯 Actionable rules\n\n- Ship\n`net/http/pprof`\n\non a private port in every service; you cannot profile what isn't instrumented.- Rising goroutine count = a leak. Check it before you check memory.\n- Use the Goroutines panel /\n`goroutine?debug=2`\n\nfor deadlocks and leaks — stacks tell you exactly who's blocked on what.`GOTRACEBACK=all`\n\nand`kill -QUIT`\n\nfor production hangs.\n\n**What:** variadic `Option`\n\nfunctions that configure a constructor. **Why:** Go has no default or keyword arguments, so a growing config would otherwise mean a growing parameter list or a mutable public struct.\n\n```\ntype Option func(*Client)\n\nfunc WithTimeout(d time.Duration) Option   { return func(c *Client) { c.timeout = d } }\nfunc WithRetries(n int) Option             { return func(c *Client) { c.retries = n } }\nfunc WithLogger(l *slog.Logger) Option     { return func(c *Client) { c.log = l } }\n\nfunc NewClient(baseURL string, opts ...Option) (*Client, error) {\n    c := &Client{baseURL: baseURL, timeout: 30 * time.Second, retries: 3, log: slog.Default()}\n    for _, opt := range opts {\n        opt(c)\n    }\n    if c.baseURL == \"\" {\n        return nil, errors.New(\"client: baseURL is required\")\n    }\n    return c, nil\n}\n\nc, err := NewClient(url, WithTimeout(90*time.Second), WithRetries(5))\n```\n\nRequired arguments stay positional; optional ones are named and additive. Adding an option never breaks an existing caller.\n\n`http.Handler`\n\n**What:** `func(http.Handler) http.Handler`\n\n. **Why:** logging, auth, tenancy, tracing, and rate limits belong around handlers, not inside them.\n\n```\nfunc Logging(next http.Handler) http.Handler {\n    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n        start := time.Now()\n        ww := &statusWriter{ResponseWriter: w, status: http.StatusOK}\n        next.ServeHTTP(ww, r)\n        slog.Info(\"request\",\n            \"method\", r.Method, \"path\", r.URL.Path,\n            \"status\", ww.status, \"ms\", time.Since(start).Milliseconds())\n    })\n}\n\nfunc Tenant(next http.Handler) http.Handler {\n    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n        t := r.Header.Get(\"X-Tenant\")\n        if t == \"\" {\n            http.Error(w, \"missing tenant\", http.StatusUnauthorized); return\n        }\n        next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), tenantKey, t)))\n    })\n}\n\nhandler := Recoverer(RequestID(Logging(Tenant(mux))))   // or r.Use(...) with chi\n```\n\nThe same shape works for any interface: wrap it, keep the type, add behaviour.\n\n`main`\n\n**What:** every dependency arrives through a constructor; `main`\n\nis the only place that knows the concrete types. **Why:** it's Go's whole DI story — no framework, no reflection, no runtime surprises. This repo's convention ([ CLAUDE.md](//CLAUDE.md)):\n\n`init()`\n\n.\"\n\n```\nfunc main() {\n    cfg, err := config.Load()\n    if err != nil { fatal(err) }\n\n    db, err := sqlx.Connect(\"pgx\", cfg.DatabaseURL)\n    if err != nil { fatal(err) }\n    defer db.Close()\n\n    var (\n        docs  = repo.NewDocs(db)                       // concrete\n        llm   = llmclient.New(cfg.APIKey)              // concrete\n        svc   = service.NewAgent(docs, llm)            // takes interfaces\n        h     = handler.New(svc)                       // takes an interface\n    )\n    …\n}\n```\n\nRead `main`\n\ntop to bottom and you know the entire architecture. Every layer is testable because every layer takes interfaces it doesn't construct.\n\n**What:** bounded parallel `map`\n\n, order preserved. **Why:** you'll write this loop in every AI service — embed, rerank, enrich, fan out to tools.\n\n```\n// ParallelMap applies f to every element with at most n concurrent calls.\n// Results keep the input order; the first error cancels the rest.\nfunc ParallelMap[T, U any](ctx context.Context, in []T, n int, f func(context.Context, T) (U, error)) ([]U, error) {\n    out := make([]U, len(in))\n    g, ctx := errgroup.WithContext(ctx)\n    g.SetLimit(n)\n    for i, v := range in {\n        g.Go(func() error {\n            u, err := f(ctx, v)\n            if err != nil {\n                return fmt.Errorf(\"item %d: %w\", i, err)\n            }\n            out[i] = u                 // distinct index per goroutine → no lock needed\n            return nil\n        })\n    }\n    if err := g.Wait(); err != nil {\n        return nil, err\n    }\n    return out, nil\n}\n\nvecs, err := ParallelMap(ctx, chunks, 8, embedOne)\n```\n\n**What:** start dependencies, block on a signal, shut down in reverse. **Why:** rolling deploys happen constantly; dropping in-flight streams on every deploy is a self-inflicted SLO breach.\n\n```\nfunc run(ctx context.Context, cfg config.Config) error {\n    ctx, stop := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)\n    defer stop()\n\n    srv := newServer(cfg)\n    errc := make(chan error, 1)\n    go func() { errc <- srv.ListenAndServe() }()\n\n    select {\n    case err := <-errc:\n        if !errors.Is(err, http.ErrServerClosed) { return err }\n    case <-ctx.Done():\n        slog.Info(\"shutting down\")\n    }\n\n    shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)\n    defer cancel()\n    return srv.Shutdown(shutdownCtx)     // stop accepting, drain in-flight\n}\n\nfunc main() {\n    if err := run(context.Background(), cfg); err != nil {\n        slog.Error(\"fatal\", \"err\", err); os.Exit(1)\n    }\n}\n```\n\nPutting the body in `run(ctx) error`\n\n— with `main`\n\nonly handling the exit code — makes the whole startup path testable.\n\n**What:** `type TenantID string`\n\n, `type Role string`\n\n. **Why:** the compiler stops you passing a user ID where a tenant ID belongs, at zero runtime cost.\n\n```\ntype (\n    TenantID string\n    DocID    string\n)\nfunc (r *Repo) Search(ctx context.Context, t TenantID, q string) ([]Doc, error) { … }\n\nr.Search(ctx, TenantID(hdr), q)     // ✅ explicit conversion at the boundary\nr.Search(ctx, userID, q)            // ❌ compile error — exactly what you want\nfunc (a *Agent) Run(ctx context.Context, userInput string) (AgentResponse, error) {\n    a.AddMessage(RoleUser, userInput)\n\n    var reply string\n    switch lower := strings.ToLower(userInput); {\n    case strings.Contains(lower, \"calculate\"):\n        expr := strings.TrimSpace(strings.SplitN(lower, \"calculate\", 2)[1])\n        res, err := a.tools.Dispatch(ctx, \"calculator\", mustArgs(\"expression\", expr))\n        if err != nil {\n            return AgentResponse{}, fmt.Errorf(\"agent.Run: %w\", err)\n        }\n        a.results = append(a.results, res)\n        reply = \"Result: \" + res.Output\n\n    case strings.Contains(lower, \"count\"):\n        res, err := a.tools.Dispatch(ctx, \"word_count\", mustArgs(\"text\", userInput))\n        if err != nil {\n            return AgentResponse{}, fmt.Errorf(\"agent.Run: %w\", err)\n        }\n        a.results = append(a.results, res)\n        top := make([]string, 0, 3)\n        for _, kv := range topN(res.Counts, 3) {\n            top = append(top, fmt.Sprintf(\"%s=%d\", kv.Key, kv.Count))\n        }\n        reply = \"Top words: \" + strings.Join(top, \", \")\n\n    default:\n        reply = fmt.Sprintf(\"Echo [%s]: %s\", a.cfg.Name, userInput)\n    }\n\n    a.AddMessage(RoleAssistant, reply)\n    return AgentResponse{\n        Messages:    a.History(),\n        ToolResults: a.results,\n        TotalSteps:  1,\n        Status:      StatusOK,\n    }, nil\n}\n```\n\nEverything in one method: `ctx`\n\nfirst, typed `Role`\n\nconstants, `switch`\n\nwith an init statement, error wrapping at every boundary, preallocated slices, and a struct return instead of a tuple.\n\n🎯 Actionable rules\n\n- Functional options for anything with more than two optional settings.\n- Wire concrete types in\n`main`\n\n; pass interfaces everywhere else.- Middleware for cross-cutting concerns;\n`defer`\n\nfor resources.- Named domain types for identifiers — free compile-time safety.\n\nTwenty-two rewrites you can apply in your next code review.\n\n```\n// ❌ the failure vanishes; the zero value flows onward\ndata, _ := json.Marshal(payload)\n// ✅ handle it, or say in writing why it can't happen\ndata, err := json.Marshal(payload)\nif err != nil {\n    return fmt.Errorf(\"handler.Query: marshal response: %w\", err)\n}\n// ❌ \"sql: no rows in result set\" — from where? which id? which layer?\nif err != nil { return err }\n// ✅ the chain reads like a stack trace you designed\nif err != nil { return fmt.Errorf(\"repo.GetDoc(%s): %w\", id, err) }\n// ❌ the success case is buried three levels deep\nif resp != nil {\n    if resp.StatusCode == 200 {\n        if body, err := io.ReadAll(resp.Body); err == nil {\n            return parse(body)\n        }\n    }\n}\nreturn nil, errors.New(\"failed\")\n// ✅ fail fast, one indent level, every error distinguishable\nif resp.StatusCode != http.StatusOK {\n    return nil, fmt.Errorf(\"llm.Complete: status %d\", resp.StatusCode)\n}\nbody, err := io.ReadAll(io.LimitReader(resp.Body, maxBody))\nif err != nil {\n    return nil, fmt.Errorf(\"llm.Complete: read body: %w\", err)\n}\nreturn parse(body)\n// ❌ leaks a connection on every error path — and exhausts the pool under load\nresp, err := client.Do(req)\nif err != nil { return err }\nbody, err := io.ReadAll(resp.Body)\nresp.Body.Close()\n// ✅\nresp, err := client.Do(req)\nif err != nil { return fmt.Errorf(\"fetch: %w\", err) }\ndefer resp.Body.Close()\n// ❌ new pool per call, and no timeout: a hung upstream hangs you forever\nresp, err := (&http.Client{}).Get(url)\njs\n// ✅ package-level, pooled, bounded (see §8.2)\nvar client = &http.Client{Timeout: 60 * time.Second, Transport: tunedTransport}\nreq, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)\nresp, err := client.Do(req)\njs\n// ❌ ~log(n) reallocations and copies\nvar out []Doc\nfor _, id := range ids { out = append(out, load(id)) }\n// ✅ one allocation\nout := make([]Doc, 0, len(ids))\nfor _, id := range ids { out = append(out, load(id)) }\n// ❌ O(n²): every += copies the whole prompt\nprompt := \"\"\nfor _, m := range history { prompt += m.Role + \": \" + m.Content + \"\\n\" }\njs\n// ✅ O(n)\nvar b strings.Builder\nfor _, m := range history {\n    fmt.Fprintf(&b, \"%s: %s\\n\", m.Role, m.Content)\n}\nprompt := b.String()\n// ❌ 10 000 goroutines, 10 000 sockets, instant 429s, no error handling\nfor _, id := range ids {\n    go fetch(id)\n}\n// ✅ at most 8 in flight, first error cancels the rest\ng, ctx := errgroup.WithContext(ctx)\ng.SetLimit(8)\nfor _, id := range ids {\n    g.Go(func() error { return fetch(ctx, id) })\n}\nif err := g.Wait(); err != nil { return fmt.Errorf(\"service.LoadAll: %w\", err) }\n// ❌ if nobody ever receives, this goroutine (and its captures) leaks forever\ngo func() { results <- expensive() }()\n// ✅ cancellation always wins\ngo func() {\n    select {\n    case results <- expensive():\n    case <-ctx.Done():\n    }\n}()\n// ❌ the client hung up 20 seconds ago; you're still paying for tokens\nfunc (s *Service) Answer(q string) (string, error) {\n    return s.llm.Complete(context.Background(), q)\n}\n// ✅ ctx first, always — cancellation flows all the way down\nfunc (s *Service) Answer(ctx context.Context, q string) (string, error) {\n    return s.llm.Complete(ctx, q)\n}\n// ❌ a missing tool and a tool with score 0 are indistinguishable\nif scores[name] == 0 { skip() }\n// ✅\nscore, ok := scores[name]\nif !ok { return fmt.Errorf(\"unknown tool %q\", name) }\n// ❌ a 9-method interface exported by the implementer — impossible to fake in a test\npackage llm\ntype Provider interface {\n    Complete(...); Stream(...); Embed(...); Tokenize(...); Models(...); /* … */\n}\n// ✅ each consumer declares the one or two methods it needs\npackage service\ntype Completer interface {\n    Complete(ctx context.Context, prompt string) (string, error)\n}\n// ❌ `go vet` error: passes a copy of the lock; the copy protects nothing\nfunc (c Cache) Get(k string) string { c.mu.RLock(); … }\n// ✅ pointer receiver, consistently across all methods\nfunc (c *Cache) Get(k string) string { c.mu.RLock(); defer c.mu.RUnlock(); … }\n// ❌ \"fatal error: concurrent map writes\" — unrecoverable, takes the process down\nvar cache = map[string][]float32{}\ngo func() { cache[k] = v }()\n// ✅ mutex next to the data it protects (or a channel-owned goroutine)\ntype Cache struct {\n    mu sync.RWMutex\n    m  map[string][]float32\n}\n```\n\n`errors.Is`\n\n, not `==`\n\n```\n// ❌ breaks the moment any layer wraps the error\nif err == sql.ErrNoRows { return NotFound }\n// ✅ traverses the whole wrap chain\nif errors.Is(err, sql.ErrNoRows) { return NotFound }\n// ❌ err != nil is TRUE even when everything succeeded\nfunc do() error {\n    var e *ToolError          // nil pointer…\n    return e                  // …wrapped in a non-nil interface\n}\njs\n// ✅ return the literal nil\nfunc do() error {\n    var e *ToolError\n    if failed { e = &ToolError{…}; return e }\n    return nil\n}\n// ❌ keeps the entire 50 MB document alive for a 100-byte snippet\nsnippet := bigDoc[:100]\ncache.Put(id, snippet)\n// ✅ copy out what you keep\ncache.Put(id, slices.Clone(bigDoc[:100]))\n// ❌ buffers the whole body, silently ignores typo'd client fields\nb, _ := io.ReadAll(r.Body)\njson.Unmarshal(b, &in)\n// ✅ streaming, bounded, strict\ndec := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20))\ndec.DisallowUnknownFields()\nif err := dec.Decode(&in); err != nil {\n    http.Error(w, \"invalid body\", http.StatusBadRequest); return\n}\n```\n\n`map[string]any`\n\n``` js\n// ❌ every number becomes float64; every access is an unchecked assertion\nvar m map[string]any\njson.Unmarshal(b, &m)\nn := int(m[\"max_tokens\"].(float64))     // panics on any surprise\njs\n// ✅ the shape is documented, validated, and autocompleted\nvar in struct {\n    MaxTokens int    `json:\"max_tokens\"`\n    Model     string `json:\"model\"`\n}\n// ❌ one malformed model response kills the process\nfunc mustParse(b []byte) Call {\n    var c Call\n    if err := json.Unmarshal(b, &c); err != nil { panic(err) }\n    return c\n}\n// ✅ expected failures are values\nfunc parseCall(b []byte) (Call, error) {\n    var c Call\n    if err := json.Unmarshal(b, &c); err != nil {\n        return Call{}, fmt.Errorf(\"agent.parseCall: %w\", err)\n    }\n    return c, nil\n}\n// ❌ the same failure appears five times in the logs, at five layers\nif err != nil {\n    slog.Error(\"query failed\", \"err\", err)\n    return err\n}\n// ✅ lower layers add context; only the handler logs\nif err != nil { return fmt.Errorf(\"repo.GetDoc: %w\", err) }   // repo\n…\nif err != nil {                                                // handler\n    slog.Error(\"query failed\", \"err\", err, \"path\", r.URL.Path)\n    http.Error(w, \"internal error\", http.StatusInternalServerError)\n}\n// ❌ what is 30? seconds? retries? tokens?\nctx, cancel := context.WithTimeout(ctx, 30)     // 30 NANOSECONDS — instant timeout\n// ✅ typed durations and named constants make the unit impossible to get wrong\nconst toolTimeout = 30 * time.Second\nctx, cancel := context.WithTimeout(ctx, toolTimeout)\ndefer cancel()\n```\n\n| Belief | Reality |\n|---|---|\n| \"Goroutines are free\" | Cheap, not free. Unbounded goroutines = unbounded memory, sockets, and downstream load. |\n| \"Channels are the answer to everything\" | A mutex around a map is simpler and faster. Channels are for transferring ownership, not for protecting state. |\n| \"Buffered channels prevent blocking\" | They delay it. A full buffer blocks exactly like an unbuffered one — the buffer just hides the backpressure until it's worse. |\n\"`close(ch)` stops the consumer\" |\nIt signals no more values. To stop work, cancel the context. |\n| \"Go has no memory leaks; there's a GC\" | Goroutine leaks, retained slice backing arrays, and unstopped tickers are all leaks the GC can't help with. |\n\"`err != nil` everywhere is boilerplate\" |\nIt's the feature. Every failure path is visible and testable — the reason Go services behave predictably. |\n| \"Empty interface = Python's dynamic typing\" |\n`any` costs you every compile-time guarantee, plus an allocation. Use concrete types. |\n| \"Go is slow at JSON / it needs a framework\" |\n`encoding/json` handles most loads; `net/http` is a production HTTP/2 server. Reach for libraries after profiling. |\n\"`sync.Map` is a faster map\" |\nIt's slower for most workloads. It exists for two specific access patterns. |\n| \"GOMAXPROCS handles containers\" | Only from Go 1.25. Before that, set it from the cgroup quota or your 500m pod spawns dozens of Ps. |\n| \"The GC keeps me under the memory limit\" | Not without `GOMEMLIMIT` . Otherwise the heap grows past the cgroup limit and the OOM killer wins. |\n| \"Generics replace interfaces\" | Different tools. Interfaces for polymorphism, generics for eliminating duplicate code over types. |\n| \"A panic in a goroutine is caught by my middleware\" |\n`recover` is per-goroutine. An unrecovered panic anywhere kills the process. |\n| \"Interfaces should be defined next to the implementation\" | Java habit. In Go the consumer declares what it needs. |\n\n**1. interface{}/any in your own APIs.** It pushes type errors to runtime and allocates. → Concrete types, or generics if you truly need several.\n\n**2. Package utils/common/helpers.** It becomes a dependency magnet and an import-cycle factory. → Name packages for what they provide:\n\n`tokens`\n\n, `retry`\n\n, `chunk`\n\n.**3. Stuttering names.** `service.ServiceAgent`\n\n, `model.ModelMessage`\n\n. → The package qualifies the name: `service.Agent`\n\n.\n\n**4. Storing context.Context in a struct.** It outlives the request and cancellation stops matching reality. → Pass it as the first parameter, every time.\n\n**5. Global mutable state.** `var db *sql.DB`\n\nat package scope makes tests order-dependent and races invisible. → Constructor injection (§13.3).\n\n**6. Giant interfaces / interfaces with one implementation.** Premature abstraction with a compile-time cost. → Write the concrete type; extract an interface at the *consumer* when a second implementation (or a test fake) appears.\n\n**7. defer inside a loop.** Resources accumulate until the function returns (§3.4). → Wrap the body in a function.\n\n**8. Ignoring rows.Err() / scanner.Err().**\n\n`for rows.Next()`\n\nending doesn't mean success — it may have failed mid-iteration. → Check the error after the loop, always.**9. Unbounded append on request data.** An unbounded history slice or in-memory result buffer is an OOM on a slow day. → Cap it: window the history, stream the results.\n\n**10. Time-based tests.** `time.Sleep(100*time.Millisecond)`\n\nto \"wait for the goroutine\" is flaky by construction. → Synchronize with a channel or `WaitGroup`\n\n; inject a clock.\n\n**11. Reinventing errgroup, singleflight, or rate.** These are hard to get right and already exist in\n\n`golang.org/x/...`\n\n.**12. log.Fatal outside main.** It calls\n\n`os.Exit`\n\n, skipping every `defer`\n\n— no flush, no shutdown, no cleanup. → Return an error; let `main`\n\ndecide.**13. Struct literals without field names.** `AgentConfig{\"a\", \"b\", 0.7}`\n\nsilently breaks when a field is inserted. → Always `Field: value`\n\n.\n\n**14. Exporting everything.** Every exported identifier is API you must keep working. → Start lowercase; export on demand.\n\n**15. Swallowing ctx.Err().** Treating cancellation as a generic failure produces 500s for clients that simply disconnected. → Check\n\n`errors.Is(err, context.Canceled)`\n\nand return early without logging noise.| Days | Focus | Ship this |\n|---|---|---|\n| 1–3 | §1–§2: syntax, slices, maps, structs | A CLI that chunks a file and prints word stats |\n| 4–6 |\n§3–§4: functions, `defer` , methods, interfaces |\nA `Tool` interface with two implementations and a registry |\n| 7–9 |\n§5: errors, wrapping, `Is` /`As`\n|\nA typed error hierarchy with sentinels and `errors.As` handling |\n| 10–14 | §6: goroutines, channels, context | A bounded worker pool that embeds 10k chunks and cancels cleanly |\n| 15–17 |\n§8: `net/http` , `json` , `slog`\n|\nA JSON API with timeouts, middleware, and graceful shutdown |\n| 18–20 | §9: streaming, retries, limits | An SSE endpoint proxying a real model with backpressure |\n| 21–23 | §10: tests, fakes, benchmarks | Table-driven tests + `httptest` + a fuzz target, all `-race` clean |\n| 24–26 | §7, §12: runtime and profiling | Profile it, cut allocations 50%, write down what you learned |\n| 27–30 | §11, §13–§15 | Distroless image, CI with lint+race, refactor against §14 |\n\n``` js\n// Declarations\nx := 5                              // infer          var x int  // zero value 0\nm := make(map[string]int, 100)      // ✅ never a nil map you write to\ns := make([]T, 0, n)                // preallocate\nv, ok := m[k]                       // comma-ok\ns = append(s, xs...)                // always reassign\n\n// Errors\nif err != nil { return fmt.Errorf(\"pkg.Func: %w\", err) }\nerrors.Is(err, ErrNotFound) · errors.As(err, &myErr) · errors.Join(errs...)\ndefer resp.Body.Close()             // right after the error check\n\n// Concurrency\ng, ctx := errgroup.WithContext(ctx); g.SetLimit(8); g.Go(func() error { … }); g.Wait()\nselect { case v := <-ch: … case <-ctx.Done(): return ctx.Err() }\nvar mu sync.RWMutex; mu.RLock(); defer mu.RUnlock()\nctx, cancel := context.WithTimeout(ctx, 30*time.Second); defer cancel()\n\n// Interfaces\ntype Completer interface { Complete(context.Context, string) (string, error) }\nvar _ Completer = (*Client)(nil)    // compile-time check\nswitch x := v.(type) { case string: … }\n\n// Format\n%v %+v %#v %q %T %w %.2f %d\n\n// Commands\ngo test -race ./... · go test -bench=. -benchmem · go test -fuzz=Fuzz\ngo vet ./... · golangci-lint run · govulncheck ./... · go mod tidy\ngo tool pprof -http=:8081 http://localhost:6060/debug/pprof/profile?seconds=30\ncurl localhost:6060/debug/pprof/goroutine?debug=2      # leak hunting\nGODEBUG=gctrace=1 ./api · GOMEMLIMIT=450MiB · kill -QUIT <pid>\n```\n\n`pkg.Func:`\n\ncontext; log once at the top.`select`\n\nhas `<-ctx.Done()`\n\n.`ctx`\n\nis the first parameter`main`\n\n`init()`\n\n.`-race`\n\nin CI, `pprof`\n\nin production.`GOMEMLIMIT`\n\nand `GOMAXPROCS`\n\nWhere to go next:[🐍 Python for AI Developers]for the other half of the stack,[🏗️ Building High-Quality AI Agents]for the architecture on top, and[🏢 Enterprise-Ready AI Agents]for multi-tenancy, security, and scale.\n\n*Go gives you fewer ways to write it, so there are fewer ways to get it wrong. Learn error, interface, defer, context, and the scheduler — the rest of the language fits on one page, which was always the point.*\n\nIf you found this helpful, let me know by leaving a 👍 or a comment!, or if you think this post could help someone, feel free to share it! Thank you very much! 😃", "url": "https://wpnews.pro/news/golang-for-ai-developers-from-0-to-pro", "canonical_source": "https://dev.to/truongpx396/golang-for-ai-developers-from-0-to-pro-1enk", "published_at": "2026-08-25 07:32:04+00:00", "updated_at": "2026-08-25 07:43:38.195319+00:00", "lang": "en", "topics": ["developer-tools", "ai-infrastructure", "artificial-intelligence"], "entities": ["Go", "Python"], "alternates": {"html": "https://wpnews.pro/news/golang-for-ai-developers-from-0-to-pro", "markdown": "https://wpnews.pro/news/golang-for-ai-developers-from-0-to-pro.md", "text": "https://wpnews.pro/news/golang-for-ai-developers-from-0-to-pro.txt", "jsonld": "https://wpnews.pro/news/golang-for-ai-developers-from-0-to-pro.jsonld"}}