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

  1. Keep packages small - Single responsibility
  2. Accept interfaces - Flexible dependencies
  3. Return structs - Clear types
  4. Handle errors early - Fail fast
  5. Document exported APIs - Clear comments

Summary

Principle Implementation
Encapsulation internal/ packages
Abstraction Small interfaces
Flexibility Options pattern
Testability Dependency injection