Go / Advanced: Safety, Performance, and Service Design / Lesson 18
API Design and Deployment Contracts
Carry language-level correctness into versioned APIs, configuration, graceful shutdown, and reproducible release artifacts.
On this page
Lesson goals
- Define validation, error, ownership, and compatibility boundaries.
- Validate configuration before starting resources.
- Plan reproducible builds and bounded shutdown without publishing automatically.
An API is more than exported signatures
A stable API defines accepted inputs, output ordering, nil and empty meaning, ownership, error categories, cancellation, and concurrency safety. Document whether callers may mutate supplied or returned slices and whether methods retain references. A signature unchanged across a release can still break these behavioral promises.
Prefer small interfaces derived from real consumers and concrete types with enforceable invariants. Keep options explicit and validate incompatible combinations. Avoid a constructor that silently starts unbounded goroutines or opens network listeners without giving the caller a shutdown handle. Starting work transfers a lifetime obligation that must be visible.
Reject invalid configuration before side effects
The first example parses a worker limit from a fixed synthetic string and enforces the application interval. It returns errors rather than silently replacing invalid input with a default. A deployment command should validate configuration before opening listeners or starting workers, so a failed configuration does not leave a partly running service.
Separate representation from policy: strconv.Atoi recognizes an integer and reports conversion problems, while the explicit range check defines the service’s worker bound. Real configuration may come from flags or environment variables, but those sources should converge on the same validation function. Never print secrets as part of a configuration diagnostic.
package main
import (
"fmt"
"strconv"
)
func workerLimit(text string) (int, error) {
value, err := strconv.Atoi(text)
if err != nil {
return 0, fmt.Errorf("worker limit: %w", err)
}
if value < 1 || value > 16 {
return 0, fmt.Errorf("worker limit outside 1..16")
}
return value, nil
}
func main() {
workers, err := workerLimit("4")
if err != nil {
fmt.Println(err)
return
}
fmt.Println("workers", workers)
}
workers 4
The synthetic value four satisfies syntax and the explicit one-through-sixteen policy.
Reject undocumented request shape
JSON decoding is a boundary contract, not an automatic business validator. The second example rejects unknown fields and requires no second JSON value after the first, then validates a nonnegative quantity. A size limit must be applied before decoding untrusted bodies; the fixed string here is already bounded synthetic input.
Unknown-field rejection is a deliberate compatibility choice. Some public APIs allow future fields; others require strict shape. Decide and document the policy rather than enabling a setting blindly. The zero value of a missing field may be indistinguishable from an explicitly supplied zero, so use pointers or another representation when presence matters.
package main
import (
"encoding/json"
"fmt"
"io"
"strings"
)
type Request struct {
Quantity int `json:"quantity"`
}
func main() {
decoder := json.NewDecoder(strings.NewReader(`{"quantity":3}`))
decoder.DisallowUnknownFields()
var request Request
if err := decoder.Decode(&request); err != nil {
panic(err)
}
var extra any
if err := decoder.Decode(&extra); err != io.EOF {
panic("trailing JSON value")
}
if request.Quantity < 0 {
panic("negative quantity")
}
fmt.Println("quantity", request.Quantity)
}
quantity 3
One known field is decoded, trailing content is rejected, and the domain check accepts three.
Record the artifact’s actual build inputs
Record the toolchain, module metadata, target GOOS and GOARCH, build tags, and relevant cgo settings. A Go binary is not universally portable to every operating system, architecture, or libc environment. Cross-compilation and static-linking assumptions depend on dependencies and configuration. Verify the artifact in an environment representative of its intended target.
Run formatting, tests, vet, supported race checks, and appropriate vulnerability analysis in a controlled release workflow. Dependency and toolchain updates require review; automatic toolchain selection should be understood rather than ignored. Build reproducibility includes generated files and configuration, not just the text of main.go.
Shutdown is part of the service API
A service should stop admission, request cancellation of owned background work, finish or reject in-flight operations under policy, and wait within a bounded cleanup lifetime. HTTP Shutdown does not automatically solve every application-owned goroutine or hijacked connection. Give each resource an owner and a completion condition, and decide what happens when the shutdown budget expires.
Expose useful health and operational signals without leaking secrets or making diagnostics public by accident. Distinguish readiness to accept work from mere process existence. Tests should cover startup failure and shutdown during blocked work. Deployment itself is a separate authorized action; a lesson or successful build does not imply permission to publish a service.
Common errors and fixes
A missing field is mistaken for an explicitly supplied zero.
- Why it happens
- The representation records value but not presence.
- How to fix it
- Use a presence-aware field or explicit validation rule.
- Prevent it next time
- Test absent, null, zero, and nonzero inputs under the chosen schema.
A server starts before configuration failure is noticed.
- Why it happens
- Validation and side effects are interleaved.
- How to fix it
- Parse and validate options before acquiring resources.
- Prevent it next time
- Test startup rejection without opening listeners.
Shutdown returns while owned workers remain blocked.
- Why it happens
- The service lacks cancellation or joining for application work.
- How to fix it
- Define stop and completion protocols for each resource.
- Prevent it next time
- Exercise shutdown while queues and sends are blocked.
Check your understanding
1. An API formerly returned a copied slice and now returns its internal cache slice with the same signature. Is this necessarily compatible?
Show worked solution
No. Callers may rely on modifying or retaining their independent result without affecting shared state. Returning an alias changes ownership and concurrency semantics even though types match. Preserve the copy, publish an explicitly borrowed immutable view with a deliberate compatibility transition, or introduce another API whose contract makes the difference clear.
2. Specify startup and shutdown behavior for a command owning an HTTP server and one worker pool.
Show worked solution
Validate configuration first, construct resources, and start work only after acquisition succeeds. On shutdown, stop new admission, cancel the pool, drain or reject in-flight work under policy, shut down HTTP with a separate bounded context, and join owned workers before final release. On partial startup failure, release only resources actually acquired and report a non-success status.
Key takeaways
- Public compatibility includes nilness, ownership, errors, and cancellation.
- Validate before side effects and record complete build assumptions.
- A deployable artifact needs a bounded startup and shutdown contract.