Packages
Packages are Go's unit of compilation and namespacing.
Introduction
Packages are Go's unit of compilation and namespacing. Every Go file belongs to a package declared at the top. The internal/ directory enforces package privacy — only code within the parent tree can import internal packages, a powerful enterprise pattern.
Exported identifiers (capitalized) form the public API; unexported ones are implementation details. Package design affects testability, circular import errors, and godoc documentation quality.
This lesson covers standard layout (cmd/, internal/, pkg/), init functions, and avoiding common import cycle pitfalls that block junior developers on day one of a new job.
The story
The Docker Engine codebase organizes code into packages: client for API calls, daemon for runtime logic, opts for flag parsing. Each directory is one package; exported names (capitalized) form the public API while lowercase helpers stay private — enforcing encapsulation without access modifiers.
Package boundaries map to team ownership in monorepos at Google: clear import paths, minimal exported surface, and internal/ directories prevent other modules from importing implementation details.
Understanding the topic
Key concepts
- package foo declares all files in directory share namespace.
- Import path is module path + /package/dir — not necessarily disk path alone.
- internal/ packages importable only from ancestor directories.
- init() runs automatically on import — order by dependency.
- go doc and pkg.go.dev document exported API.
- Circular imports are compile errors — refactor to shared package.
Step-by-step explanation
- One directory = one package (except cmd subdirs each main).
- Capitalized names exported across package boundary.
- Import groups: stdlib, blank, third-party, local — gofmt orders.
- Rename import: import f "fmt" or import . "fmt" (dot import rare).
- Blank import _ "pkg" runs init for side effects (driver registration).
- Build tags //go:build exclude files from compilation.
Practical code example
Multi-file package pattern with exported and unexported symbols:
// file: mathutil/add.gopackage mathutil// Add sums two integers — exported API.func Add(a, b int) int {return a + b}// file: mathutil/internal_helper.go (same package)package mathutilfunc validate(n int) bool {return n >= 0}func SafeAdd(a, b int) int {if !validate(a) || !validate(b) {return 0}return Add(a, b)}// file: main.gopackage mainimport ("fmt""github.com/techlearningpro/demo/mathutil")func main() {fmt.Println(mathutil.SafeAdd(10, 20))}
Line-by-line code explanation
package clientdeclares the package name — usually matches the directory name.import "github.com/org/project/client"imports using the module path fromgo.mod.ExportedFunc()starts with uppercase — visible to other packages.helper()lowercase — accessible only within the same package.internal/directory blocks imports from outside the parent tree — enforced by the compiler.init()runs automatically on import — use sparingly for registration, not heavy setup.go doc net/httprenders exported documentation from comments above declarations.import alias "long/path/pkg"shortens verbose import paths in large codebases.
Key takeaway: validate is unexported — only mathutil can call it. Split files freely within one package. internal/ dir adds stronger encapsulation at module level.
Real-world use
Where you'll use this in production
- cmd/api/main.go imports internal/service and internal/repo.
- Database driver blank import: _ "github.com/jackc/pgx/v5/stdlib".
- Shared pkg/httputil for cross-service HTTP helpers.
- Generated protobuf packages in gen/ or api/.
Best practices
- Use internal/ for code that must not be imported externally.
- Keep exported API minimal — hide helpers as unexported.
- Avoid init() logic beyond registration — hard to test.
- Structure: cmd/, internal/, optional pkg/ for public libs.
- One package per responsibility — not utils with 50 functions.
- Run go doc ./... to verify exported documentation.
Common mistakes
- Circular imports between service and handler — extract interfaces.
- Giant util package — split by domain.
- Exporting everything — leaky abstraction.
- Heavy init() causing slow imports and hidden side effects.
- Mismatch between package name and directory name — confusing imports.
Advanced interview questions
Q1BeginnerExported vs unexported?
Q2BeginnerPurpose of internal/ directory?
Q3IntermediateFix circular import?
Q4IntermediateBlank import _ purpose?
Q5AdvancedDesign package layout for payment microservice.
Summary
Packages organize code; exported names are the public API. internal/ enforces encapsulation at module level. Avoid circular imports — extract shared types/interfaces. Standard layout: cmd/, internal/, pkg/ for clarity. Next lesson: Go modules — dependency management.