Skip to content
1.1. Go

1.1. Go

In one glance

  • You will: Check and test the reference agent offline, read the dependencies it really ships, and find the package closest to its coverage floor.
  • You need: mise run install finished and mise run doctor passing from 1.0. System.
  • Time: about 18 minutes, hands-on.

Which parts of an agent are decided without a model

An agent has exactly one stochastic part: the model’s output, which can differ between two identical runs. That one part is what “you send words, you get words, and the words change on Tuesday” describes. Everything around it is a deterministic surface — the composition, the typed configuration, the tool contracts, the session store, the crash-recoverable restore, the policy plane — and all of it is ordinary Go, settled in seconds by a compiler, a linter, a race detector, and a coverage floor.

Judging those parts by running the agent instead is expensive and imprecise: every edit costs a model call that is slow on a laptop, non-repeatable, and billed per call on a hosted provider, and the model’s variance is wide enough to hide an ordinary Go bug.

This page settles that surface: mise run check and mise run test inside agents/go, the module’s real dependency inventory, and what the 80% per-package coverage floor — a regression alarm on how much code the tests reach — does and does not prove.

Predict how long that takes before you find out. Then run both commands from inside the module:

cd agents/go
mise run check
mise run test

check is formatting, linting, module tidiness, and one contract that compares the committed .env.example against the typed configuration:

[check:env-example] $ go run ./cmd/agent config:example --check ../../.env.exam…
[check:format] $ golangci-lint fmt --diff
[check:lint] $ golangci-lint run ./...
[check:tidy] $ go mod tidy -diff
[check:lint] 0 issues.
Finished in 8.36s

test runs the whole suite under the race detector, which reports two goroutines touching the same memory without synchronizing — something a green test never rules out — then measures coverage per package against the floor:

DONE 1815 tests, 1 skipped in 4.826s
[test] $ ../../scripts/check-coverage.sh coverage.out 80 agents/go
  ok      84.2%  agents/go/a2aserver
  ok      82.7%  agents/go/data
  ok      99.4%  agents/go/domain
  ok     100.0%  agents/go/model
  ok      82.7%  agents/go/state
  ok      98.5%  agents/go/tools
agents/go meets the 80% per-package coverage floor

Both are real runs with a warm build cache, and both are trimmed: the first drops each task’s timing line, the second keeps six of the coverage report’s twenty lines. The first run after a clone compiles everything and takes minutes; every run after that is roughly what you see here.

Under a minute of wall clock just exercised every part of the agent that is allowed to be exact, and not one byte of it left your machine. The part that is not exact gets judged differently later, and 0.2. Evidence keeps the two apart.

How Go composition differs from a mutable Python graph

In a Python agent framework the graph is usually something you build and then mutate: add a node, register a tool, attach a checkpointer. Here there is nothing to mutate. The graph is a value a constructor returns, and the whole conversational agent is one such value, assembled once and held. That is why the composition reads top to bottom, why a sub-agent cannot quietly acquire a tool later, and why 2.1. First Agent can treat construction order as a correctness property.

If you arrived from LangGraph or the Python course, 8.8. From Python maps that idea and the rest onto the exact package that owns it here.

What agents/go/go.mod declares, and why versions are held

The agent is a standalone Go module under agents/go, and its first require block is the honest inventory of what ships in the binary. It lists the ADK runtime, A2A, the model clients, MCP, OpenTelemetry, typed configuration, pure-Go SQLite, and the PostgreSQL driver behind the shared session backend:

require (
	github.com/a2aproject/a2a-go/v2 v2.4.0
	github.com/caarlos0/env/v11 v11.4.1
	// Three module names, one SQLite engine. github.com/glebarez/go-sqlite is the
	// database/sql driver (registered as "sqlite") that a2aserver, data, memory, and
	// state open directly; github.com/glebarez/sqlite is the GORM dialector ADK's
	// session store needs, and is a thin layer over that same driver; modernc.org/sqlite
	// is the transpiled-C engine under both and stays indirect. All three are pure Go,
	// which is what lets CGO_ENABLED=0 hold and keeps exactly one SQLite implementation
	// in the binary. Never add a cgo driver such as github.com/mattn/go-sqlite3 beside them.
	github.com/glebarez/go-sqlite v1.23.0
	github.com/glebarez/sqlite v1.11.0
	github.com/google/jsonschema-go v0.4.3
	// The second session backend, and the only non-SQLite database in the binary.
	// gorm.io/driver/postgres is the GORM dialector ADK's session store needs;
	// github.com/jackc/pgx/v5 is the pure-Go driver under it, imported for its
	// database/sql registration so cmd/agent can own and bound the pool itself.
	// Both stay cgo-free, so CGO_ENABLED=0 still holds. Sessions are the only
	// state that moves here — the incident, task, memory, and vector databases
	// remain SQLite files owned by one writer (Ch. 6.9).
	github.com/jackc/pgx/v5 v5.10.0
	github.com/modelcontextprotocol/go-sdk v1.7.0
	// ADK Go v2.2.0 owns this generated-client pair and requires openai-go v3.49.0.
	// Bump it only with ADK, so the adapter and the generated client stay in step.
	github.com/openai/openai-go/v3 v3.49.0 // compatibility hold: owner=google.golang.org/adk/v2@v2.2.0 constraint=v3.49.0 validator=agents/go mise run check and test
	// ADK Go v2.2.0 still uses the OTel log.Value and log.KeyValue APIs that the
	// 1.45/0.21 release set removed, so 1.44 with log 0.20 is the highest compiling family.
	go.opentelemetry.io/otel v1.44.0 // compatibility hold: owner=google.golang.org/adk/v2@v2.2.0 constraint=v1.44.0 validator=agents/go mise run check and test
	go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.44.0
	go.opentelemetry.io/otel/log v0.20.0 // compatibility hold: owner=google.golang.org/adk/v2@v2.2.0 constraint=v0.20.0 validator=agents/go mise run check and test
	go.opentelemetry.io/otel/metric v1.44.0
	go.opentelemetry.io/otel/sdk v1.44.0
	go.opentelemetry.io/otel/sdk/metric v1.44.0
	go.opentelemetry.io/otel/trace v1.44.0
	golang.org/x/text v0.40.0
	google.golang.org/adk/v2 v2.2.0
	google.golang.org/genai v1.66.0 // compatibility hold: owner=google.golang.org/adk/v2@v2.2.0 constraint=v1.66.0 validator=agents/go mise run check and test
	gorm.io/driver/postgres v1.6.2
)

Two things in that block repay a slow read. Three module names resolve to one SQLite engine, and the comment says which layer each one is, because adding a fourth — particularly a cgo driver — would break the CGO_ENABLED=0 build. Keeping C out of the binary is what lets it ship distroless, on a base image that carries no shell and no package manager. And four lines carry a compatibility hold marker naming an owner and a constraint: ADK v2.2.0 requires that exact OpenAI client and that OpenTelemetry family, so those pins move when ADK moves and not a release earlier. The marker also names the validator that catches a bad bump: this module’s own check and test.

One require sits just below the region, outside it on purpose: the OpenTelemetry log SDK is a direct dependency that only telemetry/export_test.go uses, to read exported records back. Everything after that is the indirect block the module solver selects. Do not hand-edit the indirect block to remove unfamiliar entries; change a direct requirement with go get or go mod tidy and review the resulting diff.

Development executables are declared separately again, through Go’s tool directive, which puts their source versions in the module graph without linking them into the agent, so a linter or a scanner is pinned for every clone while what ships stays exactly the list above:

cd agents/go
go mod verify
go tool govulncheck -version
all modules verified
Go: go1.26.6
Scanner: govulncheck@v1.6.0
DB: https://vuln.go.dev
DB updated: 2026-08-14 16:22:54 +0000 UTC

go mod verify compares every downloaded module against go.sum; it is a tamper check, not a vulnerability, licence, or provenance review, and the repository has separate checks for those. That last line is the vulnerability database’s own timestamp and will be a different date on your machine, because a scanner is only as current as the data it downloaded. The Go version itself is pinned in the root mise.toml and must equal the go directive in each module; the documentation gate fails the build when they disagree, so the pin is enforced rather than remembered.

There are three Go modules in the repository, and the separation is deliberate: agents/go is the agent, evals is a black-box evaluation harness that must reach the agent over the wire rather than by import, and tools holds repository maintenance commands. Give the harness an import path into the agent and it stops testing a service and starts testing a library.

Why the offline checks refuse to read your .env

mise run check and mise run test load no dotenv file at all.

A deterministic check should fail only because the source or the fixtures are wrong. If it also read your environment, then an expired key, a stopped model server, or a colleague’s leftover OPENAI_BASE_URL could turn a correct commit red — and, worse, a wrong commit green. The tasks that do read the repository-root .env declare it line by line in their own mise.toml: the model-backed ones such as mise run run and mise run a2a, the observability stack, the evaluation harness, and mise run config:check, whose whole job is to show you what they resolved.

Reach for the short task names rather than reconstructing raw flags:

IntentCommandWhat it touches
Synchronize the modulemise run installMay download dependencies
Rewrite Go formattingmise run formatWrites source files
Check format and lintmise run checkOffline after install
Run tests and the race detectormise run testOffline, writes coverage.out
Inspect measured coveragemise run coverageReads the profile only
Build the production binarymise run buildOffline after install

Run these inside agents/go while you change the agent, because each covers this module alone. At the repository root, mise run check and mise run test widen to all three modules, and check also sweeps the dataset, the documentation, and the infrastructure, which makes it a gate rather than an edit loop.

Your turn: find the package closest to the coverage floor

The floor is not a claim that the reached code is right. The fastest way to internalize that difference is to read the least-covered thing that still passes.

Predict before you look: which package do you expect to sit lowest — the tools, the policy plane, or the crash-recoverable state machine?

  • Mode: inspect — you are reading a measurement, not changing one.
  • Goal: name the package closest to the floor and state one behavior its coverage number does not prove.
  • Files to touch: none.
  • Preflight: cd agents/go and confirm mise run test is green, so coverage.out reflects the current tree.
  • Steps: run mise run coverage from agents/go. It reprints the per-package summary and then a per-function breakdown ending in one module total, which was 87.3% on the tree this page was written against. Pick the lowest package the summary reports as ok, open one of its test files, and read what the assertions actually claim.
  • Gate that proves completion: you can name that package, quote its percentage, and describe one failure mode the tests would not catch. On this tree two packages tie at 82.7%; a test that calls a function and checks only that it did not panic would raise both numbers and prove nothing.
  • Final state: no files changed. coverage.out is regenerated and is already ignored by Git.

Write that answer down. When Chapter 4 asks you to add an adversarial case, this exercise is the reason rather than a rule.

What you can do now

  • mise run check and mise run test both pass inside agents/go, offline, with the race detector reporting no race.
  • You can point at the block in go.mod that lists what actually ships, and explain why three SQLite module names are one engine.
  • You can say why the deterministic checks ignore your .env, and which tasks do read it.
  • You can name the package nearest the coverage floor and one thing its percentage does not prove.

“The tests pass” is no longer a single fact. You can name which parts of an agent that sentence covers, which parts it deliberately says nothing about, and roughly how long each answer takes to produce.

Continue to 1.4. Providers and give the agent a model to call. 1.2. Container Engine waits until Chapter 5 needs a container engine.