Skip to content

Chapter 7: Package Design & Modules

Good package design makes your code maintainable and reusable. Let’s explore Go’s module system and package organization best practices.

Package design is how you organize code into cohesive, reusable units. Well-designed packages have clear purposes, minimal dependencies, and intuitive APIs. Poorly designed packages create confusion, tight coupling, and maintenance nightmares.

Go’s module system, introduced in Go 1.11 and standard since Go 1.16, revolutionized dependency management. It provides versioning, reproducible builds, and a clear dependency graph. Understanding modules and package organization is essential for building maintainable Go projects.

This chapter covers module fundamentals, package naming and organization, the internal directory pattern, API design, and versioning. You’ll learn how to structure projects that scale from small utilities to large applications.

A Go module is a collection of packages with a go.mod file at the root. The go.mod file declares the module path (import path prefix) and its dependencies. Each dependency specifies a version, making builds reproducible.

Modules solved Go’s original dependency problem: “go get” fetched the latest version of dependencies, making builds unpredictable. Today, modules ensure that building your code today produces the same result as building it next year.

Key concepts:

  • Module path: Uniquely identifies your module (usually a repository URL)
  • Semantic versioning: Dependencies specify versions like v1.2.3
  • Minimal version selection: Go chooses the oldest allowed version that satisfies all constraints
  • go.sum: Checksums verify dependency integrity

Example go.mod file:

go.mod
module github.com/username/myproject
go 1.22
require (
github.com/some/dependency v1.2.3
)

Good package names are the foundation of clear APIs. Package names appear in every usage, so they should be short, memorable, and descriptive. The name should communicate what the package provides without being overly generic.

Naming rules:

  • Short - http, json, fmt - short names are easy to type and read
  • Lowercase - no underscores or mixedCaps - Go convention
  • Singular - user not users - package contains user types/functions
  • Not generic - avoid util, common, base - these reveal nothing about purpose

Why this matters: Package names become part of identifiers. user.Service is clearer than users.UserService (redundant) or common.Service (too generic). Good names make code self-documenting.

// Good
package user
package auth
package postgres
// Avoid
package utils
package helpers
package common

Package organization determines how easy your code is to navigate, test, and maintain. Go’s flat package structure (no deep nesting) encourages simple, focused packages. A well-organized project groups related functionality while keeping packages independent.

Common patterns:

  • cmd/: Entry points for multiple binaries
  • internal/: Private packages not importable by external projects
  • pkg/: Public packages intended for external use (optional)
  • api/: API definitions, protobuf files, OpenAPI specs

Key principle: Organize by responsibility, not by layer. Avoid models/, controllers/, services/ structure from other languages. Instead, group by domain: user/, order/, payment/.

A typical project structure:

myproject/
├── go.mod
├── go.sum
├── main.go # or cmd/myapp/main.go
├── internal/ # Private packages
│ ├── auth/
│ └── database/
├── pkg/ # Public packages (optional)
│ └── client/
└── api/ # API definitions
└── v1/

The internal directory restricts package access:

Go uses capitalization to control visibility:

Go modules use semantic versioning:

Terminal window
# Add a specific version
go get github.com/user/pkg@v1.2.3
# Add latest
go get github.com/user/pkg@latest
# Add specific commit
go get github.com/user/pkg@abc123
# Update all dependencies
go get -u ./...

For v2+, the import path must include the major version:

import (
"github.com/user/pkg" // v0 or v1
"github.com/user/pkg/v2" // v2.x.x
"github.com/user/pkg/v3" // v3.x.x
)

Use workspaces for multi-module development:

go.work
go 1.22
use (
./module1
./module2
./shared
)
  1. Short, clear package names - avoid generic names like util
  2. Internal packages - hide implementation details
  3. Capitalize to export - control your public API
  4. Accept interfaces, return structs - flexible inputs, clear outputs
  5. Functional options - extensible configuration
  6. Semantic versioning - v2+ in import path

Design a Package API

medium

Create a simple logger package with functional options for configuration. Support log level, output prefix, and timestamp options.


Chapter in progress
0 / 14 chapters completed

Next up: Chapter 8: Clean Architecture in Go