8 Structuring packages and services
This chapter uses a small URL-shortening service to demonstrate how to structure Go packages and services for clarity and long-term maintainability. It advocates a layered, dependency-one-way design to avoid import cycles, treating packages as focused “domain experts” rather than generic utility buckets. Clear naming, useful zero values, and minimal abstractions—leaning on the standard library—anchor the approach, with two main packages showcasing the pattern: core domain logic in link and an HTTP layer in link/rest.
At the core, link defines the Link and Key types, standardized error values, validation methods, and a Shorten function (that derives short keys deterministically) before assembling an in-memory Shortener service. The service’s zero value is made useful (lazy map init on write, safe reads from nil maps), while concurrency safety is added via a sync.RWMutex to permit many readers and a single writer. Errors are wrapped and matched with errors.Is so the HTTP layer can consistently translate domain errors into status codes without leaking internals.
The HTTP side builds simple, composable handlers—Health, Shorten, and Resolve—wired through the standard library’s ServeMux with method-aware routes and path variables. The server is configured with structured logging and protective timeouts (ReadTimeout, IdleTimeout) for robustness under load. Testing relies on httptest’s NewRecorder and NewRequest to exercise handlers directly, complemented by lightweight test helpers that improve failure reporting. Throughout, the chapter emphasizes simplicity over upfront abstractions, encouraging small, cohesive packages, late introduction of indirection, and the use of standard types that are inherently testable and easy to evolve.
The Shorten and Resolve handlers provide a REST API over HTTP using the Shortener service, which is backed by an in-memory database.
An imaginary import cycle scenario where packages import each other.
A layered package organization helps to avoid import cycles. Packages do not have a hierarchy; they are flat, meaning each package stands independently. Still, it might be helpful to think of and organize packages as if they were layered.
A layered approach to organizing packages.
Multiple goroutines using the same state may cause race conditions. A mutex allows them safe and orderly access to the shared state.
Server routes incoming HTTP requests to its Handler by calling the Handler's ServeHTTP method, passing a ResponseWriter and Request. The Handler can then use the ResponseWriter to respond to clients via the Server.
ServeMux is a Handler that routes requests to other Handlers.
Request and response lifecycle and timeout intervals.
Summary
- Go's philosophy is on the side of minimal abstractions and simplicity.
- Import cycles is a compile-time error when Package A imports B and B imports A.
- Structure packages with layers to avoid import cycles where higher-level and more specific packages depend on lower-level and generic ones.
- Focus on structuring packages as niche experts in their domains.
- Avoid generic package names in favor of clear and descriptive ones.
- Start with fewer packages and then branch out to more packages.
- Error definitions shared across the project help maintain consistency.
- Go's zero values help make types usable right away without constructors.
- Writing to a map panics if it hasn't been initialized.
- Reading a nil map returns its element type's zero value.
- Types with ready-to-use zero values often lead to more convenient-to-use types.
- Mutexes are convenient and efficient for protecting simple states. The sync.Mutex type grants exclusive access to a goroutine both for writing and reading. The sync.RWMutex type allows many concurrent readers or a single writer at a time.
- Using defer reduces the chances of forgetting to do the clean-up.
- errors.Is detects error values wrapped in chains.
- http.Handler is an interface that has a single ServeHTTP method that takes a ResponseWriter and *Request parameters and returns nothing.
- http.HandlerFunc converts an appropriate function to an http.Handler.
- http.ResponseWriter can write the handler's response to the client. It's an interface with Header, Write, and WriteHeader methods.
- http.Request contains request-related information.
- http.ListenAndServe starts an http.Server.
- http.Server serves requests with an http.Handler. Its ListenAndServe method runs a server that handles client connections and blocks until the server stops.
- log/slog is a package that provides structured logging.
- Explicitly specifying what a function requires to work improves clarity.
- Return from a handler after an error to stop further processing.
- Request.PostFormValue parses the POST body and returns the value.
- Request.PathValue returns preloaded URL segments based on a pattern.
- http.Redirect sends a 302 HTTP status code and a Location header.
- http.Error responds with an HTTP status code and a string.
- http.Server allows for registering only a single http.Handler.
- http.ServeMux is an http.Handler that can route requests to handlers.
- http.Request.PathValue extracts a wildcard segment from the URL path.
- Wildcard segments can be defined with the format "{name}".
- Timeouts can be set up to protect HTTP servers from malicious clients who may try to abuse the system with extended open connections or excessive data.
- ResponseRecorder is a ResponseWriter that records a handler's response.
- httptest.NewRequest returns a new http.Request that panics on an error.
- testing.T and testing.B satisfy the testing.TB interface.
- testing.TB.Helper marks the caller function as a test helper.
- Test helpers help to reduce code duplication and bring clarity to tests.
Go by Example ebook for free