Frontend Labs
⋯

59. Refactoring Go codebases: package seams, APIs, and compatibility

59.1 Refactor around package boundaries, not files

In Go, the package is the real unit of design. Files are just organization. Before splitting a package, ask:

  • Does the new package have a small, coherent purpose?
  • Can it avoid importing the package that imports it? If not, you are creating a cycle.
  • Is the API smaller than the implementation it hides?
  • Would a caller outside this directory understand the exported names?

The common refactor is extracting a lower-level package:

cmd/server      -> wires dependencies, parses config, starts HTTP server
internal/httpapi -> handlers, middleware, request/response types
internal/users   -> user service and business rules
internal/store   -> database implementation

Avoid extracting internal/common or internal/utils early. Those names usually mean the package has no clear owner and will accumulate unrelated helpers.

59.2 Introduce interfaces at the consumer side

Prefer this:

// internal/users/service.go
type Store interface {
	User(ctx context.Context, id string) (User, error)
	Save(ctx context.Context, u User) error
}

type Service struct {
	store Store
}

Not this:

// internal/postgres/store.go
type StoreInterface interface {
	User(ctx context.Context, id string) (User, error)
	Save(ctx context.Context, u User) error
}

The consumer knows the narrow behavior it needs. The implementation should not define a broad interface just because it exists.

59.3 Preserve API compatibility deliberately

For exported packages, changing any of these is a breaking change:

  • removing an exported name;
  • changing a function signature;
  • changing interface methods;
  • changing struct fields callers set directly;
  • changing documented error behavior.

Safer migration pattern:

// Old API kept temporarily.
func New(addr string) *Client {
	return NewWithOptions(Options{Addr: addr})
}

type Options struct {
	Addr    string
	Timeout time.Duration
	Logger  *slog.Logger
}

func NewWithOptions(opts Options) *Client {
	// new implementation
}

Deprecate, migrate call sites, then remove only at the next major version. For public modules, that often means semantic import versioning (/v2).

59.4 Mechanical refactoring checklist

go test ./...
go test -race ./...
go vet ./...
staticcheck ./...
gofmt -w .
go mod tidy

Run these before and after large moves. If the change is truly mechanical, commit it separately from behavior changes; future reviewers and debuggers will thank you.


Generated from real-life-golang-guide.md · 8 Oct 2026 16:36