12 — Modules and Packages — How Go Organizes and Versions Code
The word "package" meant something different here than in every ecosystem I'd used, which made Go's module system the slowest thing I internalized. The chain that finally held: a module is a versioned unit of code rooted at a go.mod file, a package is a directory of .go files within that module, and an import path is just the module path joined with the package's directory path. [1][5] Three layers, each with one job, and the import path is the glue. Once I could see module → package → import path as a single chain, dependency management stopped being guesswork.
Packages — the directory is the unit
A package is Go's fundamental unit of code organization: a collection of functions, types, and variables in the same directory [5]. Every .go file starts with a package declaration, and all files in one directory belong to the same package. The package name is usually the directory's name — package http in the http/ directory.
Two conventions make packages predictable. Exported identifiers (capital letters) are visible to importers; lowercase identifiers are private to the package. And package names are lowercase, single words where possible (http, fmt, json) — no snake_case or camelCase. Import paths are unique identifiers, so the same package name can appear in different paths (encoding/json and vendor/json), resolved by the full path at the import site [6].
Modules — the versioned root
A module is a collection of versioned Go packages, rooted at a go.mod file [1]. The go.mod declares:
- the module path — a unique string, usually the repository URL (github.com/aveshina/ave-api)
- the Go version the module targets
- the dependency requirements — other modules and their minimum versions
module github.com/aveshina/ave-api
go 1.22
require (
github.com/gin-gonic/gin v1.9.1
github.com/jackc/pgx/v5 v5.5.3
)The module path is the prefix of every import path inside the module. A package at github.com/aveshina/ave-api/internal/store is imported by that exact string. This is why module paths are tied to a repository URL — the path _is_ the address the Go toolchain uses to fetch the code.
The go mod commands
Three commands cover the daily workflow [2][3]:
- go mod init <module-path> creates the go.mod file. The first step of any new project.
- go mod tidy scans the source code, adds any missing requirements, and removes unused ones. It also updates go.sum, the file of cryptographic checksums that guarantees reproducible builds. Run this before any commit.
- go mod vendor copies all dependencies into a vendor/ directory inside the module, so the build works without network access [4]. Useful for air-gapped deployments and for pinning exact dependency bytes; less common now that the module cache makes online builds reliable.
Adding a dependency is go get <package>@<version> (or just go get <package> for the latest), which downloads the module to the local cache and updates go.mod. The module cache ($GOPATH/pkg/mod) is shared across projects, so a module is downloaded once.
Import rules — what is allowed
A few hard rules keep the package graph clean [6]:
- No circular imports. If package A imports B and B imports A, the compiler rejects it. This forces acyclic dependencies, which keeps builds fast and the dependency graph understandable.
- main package is the only one that compiles to an executable; everything else is a library.
- Internal packages. A directory named internal/ can only be imported by code within the subtree rooted at the parent of internal/. This is how modules mark packages that are not part of the public API.
- Capitalization is visibility. Repeated from the structs and interfaces notes because it applies to every package-level identifier.
Using third-party packages
Pulling in a library is go get, which adds it to go.mod, downloads it to the cache, and lets you import it normally [7]. The choice to take a dependency should weigh maintenance status, documentation, license, and security — Go's govulncheck (covered in the tooling notes) scans dependencies for known CVEs. The ecosystem's preference for the standard library means many problems are solved without any import at all, which keeps the dependency tree small.
Publishing modules
To share code, push the repository to a public version-control host and tag releases with semantic version tags (v1.2.3) [8]. The Go proxy system (a network of mirror servers run by Google and others) automatically discovers tagged versions and serves them to anyone running go get. There is no central registry to publish to — the version-control tag _is_ the publication. Follow semantic versioning strictly: a breaking change requires a new major version, which for v2+ must be encoded into the module path itself (/v2, /v3).
How I use this
The chain — module → package → import path — is the way of thinking I use for every Go project. I name modules by their repository URL, keep packages small and focused (one responsibility per directory), and reach for the standard library before any external dependency. go mod tidy runs before every commit, because a stale go.mod is the single most common cause of "works on my machine" build failures. And the internal/ convention is how I keep implementation packages out of the public API without a separate repo: anything under internal/ is safe to refactor because no external caller can import it. The whole system rewards small, acyclic packages, and that discipline shows up in every Go codebase I have found readable.
References
[1] The Go Authors, "Tutorial: Create a module," Go Documentation, 2024. [Online]. Available: https://go.dev/doc/tutorial/create-module
[2] The Go Authors, "Go Modules Reference," go.dev, 2024. [Online]. Available: https://go.dev/ref/mod
[3] DevTrovert, "go mod commands," blog.devtrovert.com, 2024. [Online]. Available: https://blog.devtrovert.com/p/go-get-go-mod-tidy-commands
[4] VictoriaMetrics, "Vendoring, or go mod vendor: What Is It?," 2024. [Online]. Available: https://victoriametrics.com/blog/vendoring-go-mod-vendor/
[5] The Go Authors, "os package," pkg.go.dev, 2024. [Online]. Available: https://pkg.go.dev/os
[6] DigitalOcean, "Importing Packages in Go," 2024. [Online]. Available: https://www.digitalocean.com/community/tutorials/importing-packages-in-go
[7] The New Stack, "Import and Use a Third-Party Package in Golang," 2024. [Online]. Available: https://thenewstack.io/import-and-use-a-third-party-package-in-golang/
[8] The Go Authors, "Publishing a module," Go Documentation, 2024. [Online]. Available: https://go.dev/doc/modules/publishing
Knowledge check · Question 1 of 5
What is the relationship between a module and a package?
Comments
Leave a Comment
You must be signed in to comment
0 Comments
No comments yet. Be the first to comment!