# Go: Packages, Testing, and Tools ## Learning Objectives - Understand how Go packages and modules organize code and dependencies. - Learn the visibility rules that shape API design in Go. - Write unit tests, table-driven tests, handler tests, benchmarks, and fuzz tests. - Use the Go toolchain to format, vet, benchmark, and inspect code. - Understand common package layout patterns for growing services. - Recognize how tooling discipline keeps Go codebases maintainable in production. ## Packages and Modules: Two Different Layers One of the easiest ways to get confused in Go is to blur packages and modules together. They are connected, but they are not the same thing. ### Package A package is a unit of code organization and namespace. Examples: - `fmt` - `net/http` - `time` - your own package like `internal/store` All files in a directory usually belong to the same package and compile together. ### Module A module is the versioned dependency unit defined by `go.mod`. One module can contain many packages. This distinction matters in real projects because: - packages organize design inside the codebase - modules organize dependency and version boundaries across codebases ## Why Go Organizes Code This Way Go wants dependency structure to stay visible and simple. Packages make it easy to answer questions like: - what code belongs together - what API surface is exported - what dependencies are allowed here Modules make it easy to answer questions like: - what external libraries does this project depend on - what version of a dependency are we building against - how can another project import this code reproducibly In large backend systems, those questions are not administrative details. They directly affect build speed, deploy safety, and team comprehension. ## Package Naming and Visibility ### Package Names Go package names are usually short, lower-case, and simple. Good examples: - `store` - `auth` - `queue` - `config` Less ideal examples: - `storeutils` - `commonhelpers` - `myAmazingPackage` Why the simplicity matters: - package names appear at every call site - short names keep code readable - packages should represent clear concepts, not junk drawers ### Exported vs Unexported Identifiers Go uses capitalization to control visibility. - identifiers starting with an uppercase letter are exported - identifiers starting with a lowercase letter are package-private ```go package store type User struct { ID int64 Name string } type repository struct { db *sql.DB } func New(db *sql.DB) *repository { return &repository{db: db} } ``` Why this rule exists: - it keeps visibility obvious at the point of declaration - it avoids separate access modifiers like `public` and `private` - it encourages small package APIs ### Internal APIs with `internal/` Go has a special `internal` directory rule. Packages inside `internal/` can only be imported by code within the parent module tree. This is useful when you want: - separation between application-private code and reusable libraries - freedom to refactor internals without pretending they are public APIs That makes `internal/` a strong default for service code. ## Common Package Layout Patterns There is no single mandatory project structure in Go. That is intentional. The language tries to avoid framework-enforced layout. A common service layout looks like this: ```mermaid graph TD A[payments-service] --> B[go.mod] A --> C[cmd/api/main.go] A --> D[internal/httpapi] A --> E[internal/service] A --> F[internal/store] A --> G[internal/config] A --> H[internal/worker] ``` ### `cmd/` Holds entry points for executables. If one repository produces multiple binaries, `cmd/` keeps them separate cleanly. ### `internal/` Holds application-private packages. For most services, this is where the real code lives. ### `pkg/` Some repositories use `pkg/` for packages intended to be imported by other modules. This is a convention, not a language rule. Use it only if it creates actual clarity. ### Keep Layout Earned, Not Decorative Beginners often copy large directory trees too early. If a project has only one executable and a few packages, keep it smaller until complexity genuinely arrives. ## Modules, Dependencies, and Reproducibility ### `go.mod` `go.mod` declares the module path and dependency requirements. ```go module example.com/payments go 1.25.0 require ( github.com/google/uuid v1.6.0 ) ``` ### `go.sum` `go.sum` records checksums for module content so builds can verify dependency integrity. Why this matters: - catches unexpected module tampering - helps keep builds reproducible - gives the module downloader integrity data ### How Version Resolution Works Go uses module version selection designed to be reproducible and understandable. The details can get deep, but the practical takeaway is: - dependency versions are declared in `go.mod` - the toolchain resolves one version per module in the build graph - commands like `go mod tidy` keep the graph clean Useful commands: ```bash go get github.com/google/uuid@latest go mod tidy go list -m all ``` ### Why Import Cycles Are Forbidden Go does not allow cyclic package imports. That may feel restrictive, but it enforces architectural clarity. Cycles usually signal packages that were split at the wrong boundary or abstractions that are too tangled. In large systems, this restriction prevents dependency graphs from collapsing into spaghetti. ## Designing Package Boundaries Well ### Group by Responsibility, Not by Type Name Alone Better: - `auth` - `store` - `httpapi` - `billing` Weaker: - `models` - `utils` - `helpers` Packages should usually represent behavior or domain areas, not just a pile of vaguely related structs. ### Define Interfaces Where They Are Consumed Instead of putting every interface next to every implementation, define small interfaces at the consumer boundary when that improves decoupling and testing. ### Keep APIs Small If another package imports yours, what do they truly need? In Go, smaller package surfaces are easier to maintain and easier to refactor safely. ## Testing in Go: A Built-In Culture Testing is part of normal Go workflow, not an optional afterthought. ### Where Tests Live Tests live in files ending with `_test.go`. ```text store/ store.go store_test.go ``` Functions that begin with `Test` are discovered by `go test`. ```go func TestParsePort(t *testing.T) { port, err := parsePort("8080") if err != nil { t.Fatalf("unexpected error: %v", err) } if port != 8080 { t.Fatalf("got %d want %d", port, 8080) } } ``` ### Why Go's Testing Model Is Simple The standard library's `testing` package is deliberately small. That simplicity means: - the barrier to writing tests is low - teams do not need a huge framework to get started - test execution integrates naturally with the language toolchain ## Table-Driven Tests This is one of the most common idioms in Go. ```go func TestParsePort(t *testing.T) { tests := []struct { name string input string want int wantErr bool }{ {name: "valid", input: "8080", want: 8080}, {name: "invalid", input: "abc", wantErr: true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := parsePort(tt.input) if tt.wantErr { if err == nil { t.Fatalf("expected error, got nil") } return } if err != nil { t.Fatalf("unexpected error: %v", err) } if got != tt.want { t.Fatalf("got %d want %d", got, tt.want) } }) } } ``` Why this pattern is so useful: - test cases stay compact - edge cases become easy to enumerate - adding new scenarios becomes mechanical rather than repetitive ## Testing HTTP Handlers For APIs, `net/http/httptest` is essential. ```go func TestHealthHandler(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/health", nil) rec := httptest.NewRecorder() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"status":"ok"}`)) }) handler.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("got %d want %d", rec.Code, http.StatusOK) } } ``` Why this matters: - lets you test handlers without a real network listener - keeps tests fast and deterministic - makes request/response behavior easy to inspect ## Benchmarks Performance-sensitive code can be benchmarked with the same toolchain. ```go func BenchmarkParsePort(b *testing.B) { for i := 0; i < b.N; i++ { _, _ = parsePort("8080") } } ``` Run it with: ```bash go test -bench=. -benchmem ./... ``` ### Why Benchmarks Matter In backend systems, intuition about performance is often wrong. Benchmarks let you compare alternatives with data. Be careful though: - microbenchmarks can miss real I/O behavior - unrealistic inputs produce misleading results - one benchmark is not a system profile ### Allocation Awareness `-benchmem` shows allocation counts and bytes per operation. This is particularly helpful in Go because excess allocation often increases garbage collection pressure. ## Fuzz Testing Modern Go supports fuzz testing through the standard toolchain. ```go func FuzzParsePort(f *testing.F) { f.Add("8080") f.Add("abc") f.Fuzz(func(t *testing.T, input string) { _, _ = parsePort(input) }) } ``` Why fuzzing is valuable: - explores unexpected input combinations - finds parser and validation edge cases - is especially useful for network protocols, text parsing, and serialization code ## Tooling That Should Be in Your Daily Workflow ### `go fmt` ```bash go fmt ./... ``` Standard formatting is one of Go's biggest team-level productivity wins. ### `go test` ```bash go test ./... ``` Run it constantly. Fast feedback is part of idiomatic Go engineering. ### `go test -race` ```bash go test -race ./... ``` This detects many data races in concurrent code. If you write goroutines and shared state, this is a critical tool. ### `go vet` ```bash go vet ./... ``` `go vet` looks for suspicious constructs that compile but are likely wrong. ### `go test -cover` ```bash go test -cover ./... ``` Coverage is useful as a signal, not a religion. High coverage does not guarantee meaningful tests, but very low coverage may show untested risk. ### Useful External Tools - `staticcheck` for deeper linting and bug finding - `govulncheck` for known vulnerability detection in dependencies and reachable code - `goimports` for formatting plus import cleanup - `pprof` for CPU and memory profiling These are not built into the core language, but they fit naturally into Go's tooling culture. ## Build and Release Workflows Go makes builds operationally simple. ### Build a Binary ```bash go build ./cmd/api ``` ### Cross-Compile ```bash GOOS=linux GOARCH=amd64 go build ./cmd/api ``` This is a practical reason Go is so common in infrastructure tools. Cross-platform binaries are relatively easy to produce. ### Why Single-Binary Delivery Matters In deployment pipelines, simplicity is leverage. - containers become smaller and simpler - startup is predictable - dependency packaging becomes easier - CI artifacts are easier to reason about ## Documentation as Part of the API Surface Go places real value on package and symbol documentation. Exported identifiers should usually have comments when the package is intended for reuse. Why this matters: - tooling can surface docs automatically - package APIs become easier to consume without reading implementation details - maintainers communicate intent, not just mechanics ## Real-World Usage Patterns ### Service Repository Layout A production service often has: - one or more `cmd/` entry points - internal packages for handlers, business logic, storage, and config - tests next to each package - benchmark and race-check jobs in CI ### Testing Strategy Healthy Go services usually combine: - unit tests for pure logic - handler tests with `httptest` - integration tests for DB and network boundaries - benchmarks for hot paths - race detection for concurrent subsystems ### Tooling in CI A practical CI pipeline often runs: - `go fmt` or formatting checks - `go test ./...` - `go test -race ./...` - `go vet ./...` - optional static analysis and vulnerability scanning ## Common Mistakes and Misconceptions ### Mistake: Creating Huge Project Layouts Too Early Structure should reflect real complexity. Decorative folders make learning and maintenance harder. ### Mistake: Treating `pkg/` as Mandatory It is only a convention. Many good Go services never use it. ### Mistake: Exporting Too Much Large public package surfaces make refactoring harder and couple packages unnecessarily. ### Mistake: Ignoring Import Cycles Until Late If your packages keep wanting to import each other, the boundaries are probably wrong. ### Mistake: Writing Only Happy-Path Tests Most production failures happen in error paths, edge inputs, and timeout scenarios. ### Mistake: Optimizing from Guesswork Instead of Benchmarks Measure before changing code for performance reasons. ### Mistake: Treating Coverage as the Goal Coverage can be useful, but well-chosen tests matter more than inflated percentages. ## Summary Go packages and modules keep code organization and dependency management explicit. The testing package and standard toolchain make quality checks part of ordinary development rather than an extra framework burden. The big practical lessons are: - packages shape design boundaries - modules shape dependency boundaries - small APIs and clean imports keep code maintainable - table-driven tests and `httptest` are core testing patterns - benchmarks, race checks, and tooling provide objective feedback - operational simplicity is one of Go's biggest strengths The next step is to bring all of this together in production system design: HTTP servers, request lifecycles, context propagation, timeouts, graceful shutdown, observability, and distributed systems patterns in Go.