Skip to content

Orientation

This is the on-ramp. If you’re an experienced engineer new to Go, start here: we’ll learn Go by reading a real production codebase rather than working through toy examples. The running example throughout this guide is a distributed SQL proxy — a real, well-known architecture, written in Go — and this page covers what it is, how its code is laid out, and — most importantly — how to read unfamiliar Go fast enough to find your way around a large codebase.

The “how to read Go” material here is language-general; it pays off in any Go project you ever touch.

The example system is a distributed SQL proxy: a set of small Go services that sit in front of real PostgreSQL servers and add horizontal scaling, connection pooling, and automated failover. (This is a well-trodden architecture — Vitess, PgBouncer, and ProxySQL all do a version of it.) An application speaks the ordinary PostgreSQL wire protocol; it never knows it is talking to a proxy. Behind the proxy, the system decides which Postgres to run a query on, reuses connections, and — when a Postgres dies — elects a new leader and reconnects, all without the client noticing.

It is a single Go module, github.com/example/platform, on Go 1.25, laid out in the standard cmd/ + internal/ style. The wire protocol and gRPC contracts are its stable spine.

The mental model: service topology and the request path

Section titled “The mental model: service topology and the request path”

There are five long-running services (implementations under internal/) plus operator and test binaries. The latency-sensitive path is just two hops, and only the first hop is gRPC — the second is a real pooled SQL connection:

Request path
Rendering diagram…

Everything else is off the query path:

Service Role On the query path?
gateway Stateless proxy: speaks PG wire + gRPC, routes queries Yes (hop 1)
pooler Connection pooling; serves queries; talks to dbctld via gRPC Yes (hop 2)
dbctld PostgreSQL control daemon — lifecycle only (start/stop/restart) No (control plane)
orchestrator Consensus + failover orchestration No (control plane)
admin Admin service, HTTP + gRPC endpoints No (admin)

Two binaries under cmd/ are not services: platformctl is the operator CLI, and portpoolserver is a cross-process port allocator used only by integration tests.

The cluster is organized into cells (availability zones), with metadata in a topology store (etcd in production). For the full cell-aware topology, the leader/primary terminology, and the exact query trace, see the deep dive: Architecture & Request Flow.

The repo root holds the contracts and config; the Go source follows the standard cmd/ + internal/ layout.

Path What lives here
proto/ gRPC/protobuf contracts (.proto). Source of truth for service surfaces; generates into pb/. Key files: poolerservice.proto, dbctldservice.proto, orchservice.proto, gatewayservice.proto, clustermetadata.proto, query.proto.
config/ Per-component YAMLs (e.g. gateway.yaml, pooler.yaml, dbctld.yaml, orch.yaml) — small files holding things like http-port and log-level. They are one source in a precedence chain (flags > env > config file > defaults); cluster-wide metadata lives in the topology store, not here.
Makefile Self-documenting build/dev driver (make help).
cmd/ / internal/ / … All Go source — see below.

The Go tree:

Path What lives here Dependency rule
cmd/ 7 binaries (the 5 services + platformctl CLI + portpoolserver). Each is a tiny main.go. Can depend on anything.
internal/ (services) The 5 long-running services’ implementations (admin, gateway, orch, pooler, dbctld). Cannot depend on cmd/ or other services.
internal/ (shared) Shared code: errx, pgprotocol, sqltypes, parser, queryservice, topo, consensus, constants, etc. Cannot depend on cmd/ or the services.
tools/ Generic, project-agnostic helpers (timers, retry, …). Cannot depend on any repo code outside tools/.
pb/ Generated protobuf Go (// Code generated). Read-only.
observability/ Metric catalog.
provisioner/ Cluster provisioning (local + provisioner.go).
test/ End-to-end tests and test utilities (endtoend, utils).

The dependency direction is strict and worth internalizing — it tells you which way imports may point:

Allowed dependency directions
Rendering diagram…

In words: cmd/ may depend on anything; the services may not depend on cmd/ or on each other; the shared libraries may not depend on cmd/ or the services; and tools/ may not depend on anything outside tools/.

Go has almost no metaprogramming, so the code says what it does. The leverage is in knowing where to start and how the pieces are wired.

github.com/example/platform/internal/gateway is the directory internal/gateway. Strip the module prefix (github.com/example/platform) and you have a path relative to the repo root. This mapping is exact and always holds.

Every binary is a tiny cmd/<svc>/main.go that delegates immediately. There are two shapes.

Service binaries build a cobra command, then run an Init / RunDefault pair. From cmd/gateway/main.go:

cmd/gateway/main.go
func run(ctx context.Context, gw *gateway.Gateway) error {
if err := gw.Init(ctx); err != nil {
return err
}
return gw.RunDefault()
}

The package doc-comment and the cobra Short string are the project’s own one-line descriptions — read them first. (The gateway’s Short: “gateway is a stateless proxy responsible for accepting requests from applications and routing them to the appropriate pooler server(s) for query execution. It speaks both the PostgreSQL Protocol and a gRPC protocol.”)

The operator CLI has a different shape — cmd/platformctl/main.go just dispatches to subcommands (which live under cmd/platformctl/command/):

cmd/platformctl/main.go
func main() {
root := command.GetRootCommand()
if err := root.Execute(); err != nil {
os.Exit(1)
}
}

The request path is glued together by interfaces, not concrete types. When you hit one — engine.IExecute, queryservice.QueryServicegrep for its name to find the definition and the concrete implementation(s):

Terminal window
grep -rn 'IExecute' .

This is the single most useful habit for reading Go service code: an interface defines the seam, and the implementation lives in some other package you find by grepping the name. See Interfaces & composition for why Go is structured this way.

These are general Go techniques:

  • go doc <import-path> prints a package’s doc-comment and exported symbols. go doc <import-path>.Symbol drills into one type or function. Faster than opening the file when you just want the contract.
  • gopls is the Go language server (jump-to-definition, find-references). If your editor has Go support, it is running gopls.

The guide is organized into five tracks. The language and project tracks are the spine; the tooling and reference tracks support them.

The language track presupposes nothing but general programming experience; the project track presupposes the whole language track. Within each track, read in order — the numbering encodes a real dependency chain, not just a sequence.

A few workflow facts worth knowing before you build anything:

  • Build with make build; regenerate code with make proto / make parser; do both plus binaries with make build-all.
  • This codebase runs its tests through a dev wrapper rather than calling go test directly — integration tests auto-build first. See the testing workflow.