Designing for the Long Term
Overview
Maintainable Go code follows consistent patterns and architectural principles.
Package Design
myapp/
├── cmd/ # Entry points
│ └── server/
├── internal/ # Private packages
│ ├── auth/
│ ├── database/
│ └── handlers/
├── pkg/ # Public libraries
└── api/ # API definitions
Dependency Direction
cmd → internal → pkg
↓
external
Lower layers should not import higher layers.
Interface Segregation
// Small, focused interfaces
type Reader interface { Read([]byte) (int, error) }
type Writer interface { Write([]byte) (int, error) }
// Compose when needed
type ReadWriter interface {
Reader
Writer
}Options Pattern
type Server struct {
host string
port int
timeout time.Duration
}
type Option func(*Server)
func WithPort(p int) Option {
return func(s *Server) { s.port = p }
}
func NewServer(opts ...Option) *Server {
s := &Server{host: "localhost", port: 8080}
for _, opt := range opts {
opt(s)
}
return s
}Error Handling
// Wrap with context
return fmt.Errorf("repository.GetUser: %w", err)
// Custom error types for inspection
type NotFoundError struct{ ID int }
func (e NotFoundError) Error() string { ... }Testing Strategy
- Unit tests for business logic
- Integration tests for boundaries
- Table-driven tests for coverage
- Mocks for external dependencies
Guidelines
- Keep packages small - Single responsibility
- Accept interfaces - Flexible dependencies
- Return structs - Clear types
- Handle errors early - Fail fast
- Document exported APIs - Clear comments
Summary
| Principle | Implementation |
|---|---|
| Encapsulation | internal/ packages |
| Abstraction | Small interfaces |
| Flexibility | Options pattern |
| Testability | Dependency injection |