Overview

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.

FAQ

How does a layered package structure help avoid import cycles in Go?A layered structure lets higher-level packages depend only on lower-level ones, never the other way around. For example, link/rest can import link, but link must not import link/rest. Keeping a single import direction prevents cycles, clarifies responsibilities, and makes the codebase easier to reason about.
Should I worry about name stuttering like link.Link? Is it idiomatic?Stuttering is a guideline, not a strict rule. Using link.Link can be acceptable because the type name is clear within its own package and mirrors common standard library patterns (for example, url.URL, time.Time). Avoid unnecessary repetition elsewhere (for example, prefer link.Shortener over link.LinkShortener).
Why make a type’s zero value useful, and how was it applied here?Making zero values useful removes boilerplate and reduces errors. The Shortener type uses a nil map field that’s lazily initialized on first write, so you can use a zero Shortener directly without a constructor. Reads from a nil map are safe in Go (they return the element type’s zero value).
When should I use a mutex instead of channels to protect shared state?Use mutexes when you need to synchronize access to shared memory directly. Channels are great for coordinating goroutines but add overhead if used as a lock substitute. A simple sync.Mutex or sync.RWMutex is usually clearer and faster for protecting shared maps and counters.
What’s the difference between Mutex and RWMutex, and how do I choose?Mutex allows only one goroutine to access the critical section at a time. RWMutex lets many readers proceed concurrently but allows only one writer, blocking readers during writes. RWMutex helps in read-heavy workloads, but its benefits diminish with frequent or long writes—benchmark for your case.
How do I build HTTP handlers using only the standard library?Implement functions with signature func(w http.ResponseWriter, r *http.Request). Convert them to handlers with http.HandlerFunc when needed. Use http.NewServeMux to route requests by method and path (for example, "POST /shorten", "GET /r/{key}"), and extract route variables with r.PathValue("key").
How should I map domain errors to HTTP status codes?Use package-level sentinel errors (for example, ErrBadRequest, ErrConflict, ErrNotFound, ErrInternal) and map them via errors.Is to the corresponding status codes. Respond with http.Error and log internal issues while masking internal details from clients for security and clarity.
How can I configure timeouts to make my HTTP server resilient?Create an http.Server explicitly and set ReadTimeout and IdleTimeout. ReadTimeout limits how long the server waits to read the full request; IdleTimeout limits how long to keep idle keep-alive connections open. These protect the server from slow or unresponsive clients.
What’s the recommended way to test HTTP handlers?Use the httptest package: create a request with httptest.NewRequest, capture output with httptest.NewRecorder, call the handler, then assert on status code and body. Test helpers built on testing.TB and t.Helper can reduce duplication and produce clearer failure messages.
What does the internal directory do, and when should I use it?Placing packages under an internal directory restricts their import to sibling packages within the same parent. This enforces encapsulation and signals “not part of the public module API,” allowing internal code to evolve without breaking external users.

pro $24.99 per month

  • access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!
  • choose one free eBook per month to keep
  • exclusive 50% discount on all purchases
  • renews monthly, pause or cancel renewal anytime

lite $19.99 per month

  • access to all Manning books, including MEAPs!

team

5, 10 or 20 seats+ for your team - learn more


choose your plan

team

monthly
annual
$49.99
$499.99
only $41.67 per month
  • five seats for your team
  • access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!
  • choose another free product every time you renew
  • choose twelve free products per year
  • exclusive 50% discount on all purchases
  • renews monthly, pause or cancel renewal anytime
  • renews annually, pause or cancel renewal anytime
  • Go by Example ebook for free
choose your plan

team

monthly
annual
$49.99
$499.99
only $41.67 per month
  • five seats for your team
  • access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!
  • choose another free product every time you renew
  • choose twelve free products per year
  • exclusive 50% discount on all purchases
  • renews monthly, pause or cancel renewal anytime
  • renews annually, pause or cancel renewal anytime
  • Go by Example ebook for free
choose your plan

team

monthly
annual
$49.99
$499.99
only $41.67 per month
  • five seats for your team
  • access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!
  • choose another free product every time you renew
  • choose twelve free products per year
  • exclusive 50% discount on all purchases
  • renews monthly, pause or cancel renewal anytime
  • renews annually, pause or cancel renewal anytime
  • Go by Example ebook for free