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. A List[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 a Read([]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.