10. Architecture in Go
Structure large repos for ownership and testability; favor clean boundaries and explicit wiring.
Question: How do you structure a large Go project for maintainability and clear ownership?
Answer: I follow a structure that separates concerns and enforces clear boundaries, often aligned with Clean Architecture or Hexagonal Architecture principles. A standard layout includes a cmd
directory for the main
package, an internal
directory for all core application logic, and a pkg
directory for genuinely reusable libraries.
Explanation: The internal
directory is key; the Go toolchain enforces that packages inside internal
cannot be imported by external projects, preventing tight coupling. Within internal
, I would create packages that separate concerns, for example:
internal/http
: HTTP handlers, routing, and middleware.internal/service
: Contains the core business logic, orchestrating different components.internal/repo
: Handles data persistence and interacts with the database.
The service layer should depend on interfaces that the repo layer implements, inverting the dependency and making the application easier to test and adapt.
# Project Skeleton Example
cmd/app/main.go
internal/
http/ # Handlers, middleware
service/ # Domain logic
repo/ # Database access
telemetry/ # Logs/metrics/tracing setup
pkg/ # Reusable libraries, e.g., pkg/my-rpc-client
Question: When would you use generics versus interfaces in Go? What are the trade-offs?
Answer: Use generics when you need to write functions or data structures that work with a variety of specific, concrete types, while preserving type safety and avoiding runtime type assertions. Use interfaces when you need to define a contract for behavior (a set of methods) that different types can implement.
Explanation:
- Generics (Type Parameterization): Excellent for data structures like trees, linked lists, or utility functions like
map
,filter
,reduce
. The key benefit is static type safety. AList[string]
will only ever contain strings, which the compiler guarantees. This often leads to better performance than interface-based approaches because it avoids the overhead of dynamic dispatch and type assertions. - Interfaces (Polymorphism): Ideal for abstracting behavior. For example, an
io.Reader
interface allows a function to accept any type that can provide aRead([]byte) (int, error)
method, regardless of what the underlying type is (a file, a network connection, an in-memory buffer). Interfaces are about what a type can do, while generics are about working with any specific type.
Question: How do you approach dependency injection and configuration?
Answer: Prefer constructor functions that accept interfaces and configuration structs; wire with small composition roots. Tools like google/wire
can generate DI code.
Explanation: Explicit wiring improves testability and avoids global state. Read config from env/flags with validation and defaults.
Question: How do you organize background jobs and shutdown?
Answer: Run background goroutines tied to a root context; use errgroup.WithContext
and listen for signals to initiate graceful shutdown.
Explanation: Ensures no leaks and graceful draining of work.
Question: How do you manage multi-module repos?
Answer: Use go work
to compose modules locally without changing import paths; keep clear ownership boundaries between modules.
Explanation: Improves developer workflows across services/libraries.