How to Organize a Go Project Structure for Maintainability and Scalability
The best way to organize a Go project is to use a single module at the repository root with a clear directory layout separating commands (cmd/), public libraries (pkg/), and private implementation details (internal/), following the conventions established by the Go standard library itself.
Organizing a Go project structure correctly from the start prevents technical debt and makes large codebases navigable. The Go project (golang/go) maintains one of the most scalable codebases in the industry, and its layout in the src/ directory serves as the canonical reference for these patterns.
Use a Single Go Module at the Repository Root
Place one go.mod file at the top-level of your repository to version the entire tree as a single module. This ensures consistent dependency resolution across all packages and commands within the project.
The Go repository follows this pattern. In src/go.mod, the standard library declares itself as the std module:
module std
go 1.22
This single module approach eliminates the complexity of nested modules while allowing the go command to resolve imports consistently across the entire codebase.
Follow a Clear Top-Level Directory Layout
A predictable directory structure makes the codebase self-documenting. Organize your Go project structure using these standard directories:
cmd/
Place one subdirectory per executable command, named after the binary it produces. Each subdirectory contains a main.go file that imports and invokes library code.
The Go repository demonstrates this in src/cmd/go/main.go, which implements the go command itself:
package main
import (
"os"
"cmd/go/internal/base"
)
func main() {
base.Main()
}
internal/
Store packages that are implementation details and should not be imported by external modules. The Go compiler enforces this restriction, preventing accidental external dependencies.
For example, src/internal/trace/tracev2/trace.go contains tracing internals that are not part of the public standard library API:
package trace
// Internal implementation details...
pkg/
House public libraries intended for import by other projects. While the Go standard library places public packages directly under src/ (e.g., src/net/http), application projects often use pkg/ to clearly distinguish reusable code from implementation details.
api/, configs/, scripts/, test/, examples/
- api/: Versioned API contracts (Protobuf, OpenAPI specs)
- configs/: Default configuration files (YAML, JSON)
- scripts/: CI/CD scripts, code generation helpers
- test/: Integration and end-to-end tests spanning multiple packages
- examples/: Demonstration programs that serve as executable documentation
Maintain Sensible Package Granularity
Organize code into cohesive packages where each package has a single, clear purpose. Avoid "god" packages that accumulate unrelated functionality.
Follow the standard library's pattern of nested packages for related domains. For instance, net/http, net/url, and net/mail share the net parent but remain separate packages with distinct responsibilities.
Write Self-Contained Tests Next to Source Code
Place test files in the same directory as the code they test, using the _test.go suffix. This co-location ensures tests remain synchronized with implementation changes.
Include example functions (ExampleX) in these files. The go test command compiles and runs these as both tests and documentation, as seen in src/net/http/example_test.go.
Document Public APIs with Go Doc Comments
Every exported identifier must have a comment beginning with the identifier's name. This enables go doc and pkg.go.dev to generate useful documentation.
The Go repository exemplifies this in src/net/http/server.go, where exported types and functions include comprehensive documentation comments that explain behavior, parameters, and return values.
Version Your Module with Semantic Tagging
Tag releases using semantic versioning (vX.Y.Z) so dependent projects can pin to specific versions using go get. The Go project itself tags releases as go1.22.0, which the module system interprets according to semantic versioning rules.
Implement Continuous Integration and Linting
Configure CI pipelines to run go test ./... on every pull request. Include static analysis using go vet and tools like golangci-lint to catch style violations and potential bugs before they reach production.
Follow Go Naming Conventions
Use short, lowercase package names without underscores. Directory names should match package names, making import paths predictable. For example, the bytes package lives in the bytes directory, and the encoding/json package lives in src/encoding/json.
Isolate Generated Code
Store generated files with a *_gen.go suffix or in a dedicated gen/ subdirectory. Include a //go:generate directive in the source package that specifies the command to regenerate the files, ensuring the generation process is reproducible and documented.
Keep the Repository Tidy with Documentation
Include a README.md at the repository root explaining the project's purpose, build instructions, and high-level architecture. For library projects, consider a doc.go file that provides package-level documentation.
Summary
- Use a single module at the repository root with a
go.modfile to unify dependency management across the entire project. - Separate concerns using
cmd/for executables,internal/for private implementation details, andpkg/for public libraries. - Co-locate tests with source code using
_test.gofiles and include example functions for documentation. - Document everything using Go doc comments that start with the identifier name for all exported symbols.
- Version releases using semantic tags (
vX.Y.Z) and enforce quality with CI pipelines runninggo test ./...and linting tools.
Frequently Asked Questions
What is the difference between the internal/ and pkg/ directories in a Go project?
The internal/ directory contains packages that are implementation details and cannot be imported by code outside the module, enforced by the Go compiler. The pkg/ directory contains public libraries intended for external consumption. Use internal/ to hide complexity and prevent external dependencies on unstable APIs, while pkg/ exposes stable, documented functionality for other projects to import.
Should I use nested modules or a single module for my Go project?
For most projects, a single module at the repository root is the best practice. This simplifies dependency management, ensures consistent versioning across the codebase, and aligns with how the Go standard library itself is organized (using src/go.mod). Nested modules add complexity and should only be used when you have truly independent components with different release cycles that must be versioned separately.
How does the Go standard library organize its commands and packages?
The Go standard library places executable commands in src/cmd/ (such as src/cmd/go/main.go for the go tool itself) and public packages directly under src/ (such as src/net/http). Private implementation details that should not be imported externally are placed in src/internal/ (like src/internal/trace/tracev2/trace.go). This layout demonstrates the clear separation between user-facing commands, public APIs, and internal implementation details.
Where should I place integration tests in a Go project?
Place integration tests that span multiple packages in a dedicated test/ directory at the repository root, or use the *_test.go convention within specific packages when testing package boundaries. The Go repository uses src/cmd/go/testdata extensively for integration test data and scripts. For end-to-end tests, a top-level test/ directory keeps them separate from fast unit tests while still allowing them to import the module's public packages from pkg/ or internal helpers from internal/ as needed.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →