Modules Deep Dive
npm vs Go modules
Section titled “npm vs Go modules”If you know npm you already understand 90% of Go modules. Both systems use semantic versioning, lock files, and a manifest file. The mental model maps cleanly — but the mechanics differ in important ways.
// package.json{ "name": "my-app", "version": "1.0.0", "dependencies": { "express": "^4.18.2", "lodash": "^4.17.21" }, "devDependencies": { "jest": "^29.0.0" }}// go.modmodule github.com/myorg/my-app
go 1.21
require ( github.com/gin-gonic/gin v1.9.1 github.com/stretchr/testify v1.8.4)
// No separate devDependencies in Go.// Test-only deps are included in require; the// compiler excludes them from production binaries.go.mod — the manifest
Section titled “go.mod — the manifest”go.mod is the equivalent of package.json. It has three required fields:
module github.com/myorg/my-app ← module path (import prefix)go 1.21 ← minimum Go versionrequire (...) ← direct dependenciesKey differences from package.json:
- The module path is a URL-like string, usually matching the VCS repo path.
- There is no
devDependencies. All dependencies live inrequire. The Go linker only includes packages actually imported by the binary being built. ^range syntax does not exist. You pin an exact version (e.g.,v1.9.1).
go.sum — the lock file
Section titled “go.sum — the lock file”go.sum is the equivalent of package-lock.json. It records cryptographic hashes for every dependency version:
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BerypkHKpy7LtzLuviaCYD2h8zebo=github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=Two hash lines per dependency:
h1:...— hash of the module zip content/go.mod h1:...— hash of just thego.modfile
Both must match on every machine and in CI — go mod verify checks this. You commit go.sum to version control (just like package-lock.json).
Common module commands
Section titled “Common module commands”# Initialise a new modulego mod init github.com/myorg/my-app
# Add a dependency (updates go.mod + go.sum)
# Upgrade to latest patch/minorgo get github.com/gin-gonic/gin@latest
# Remove unused dependenciesgo mod tidy
# Verify all downloaded modules match go.sumgo mod verify
# List all direct + indirect dependenciesgo list -m all
# Show available versions of a modulego list -m -versions github.com/gin-gonic/ginMajor version semantics
Section titled “Major version semantics”Go modules encode the major version in the import path for v2+. This is the most surprising difference from npm:
// npm — same package name, just a new version// package.json"dependencies": { "some-lib": "^3.0.0" // v3, same import}// import someLib from 'some-lib'; ← unchanged// Go — v2+ changes the import path// go.modrequire github.com/some-lib/core v2.0.0+incompatible
// If the library has a proper /v2 module:require github.com/some-lib/core/v2 v2.0.0
// Your imports change:import "github.com/some-lib/core/v2"// v1: import "github.com/some-lib/core"// v2: import "github.com/some-lib/core/v2"replace directive — local development
Section titled “replace directive — local development”When you are developing a dependency alongside your main module, replace maps a module path to a local directory — equivalent to npm link or file: paths in package.json:
require github.com/myorg/shared-lib v1.2.3
replace github.com/myorg/shared-lib => ../shared-libRemove replace before committing to production. Use go mod tidy afterward to clean up.
Vendoring
Section titled “Vendoring”Vendoring copies all dependencies into a vendor/ directory inside your repo — useful for air-gapped builds or reproducibility guarantees without relying on the module proxy:
# Create/update vendor directorygo mod vendor
# Build using only vendored deps (ignores module cache)go build -mod=vendor ./...
# Verify vendor matches go.sumgo mod verifyGo workspaces (go work)
Section titled “Go workspaces (go work)”Workspaces (added in Go 1.18) let you develop multiple inter-dependent modules simultaneously without replace directives — equivalent to npm workspaces or yarn workspaces:
// npm workspaces — package.json at root{ "workspaces": [ "packages/api", "packages/shared" ]}// Go workspaces — go.work at repo rootgo 1.21
use ( ./api // module: github.com/myorg/api ./shared // module: github.com/myorg/shared)
// Now api can import shared directly from disk,// no replace directive needed.# Initialise a workspace (creates go.work)go work init ./api ./shared
# Add another module to the workspacego work use ./cli
# Sync go.work.sum (lock file for workspaces)go work syncgo.work is typically git-ignored (it is a local developer convenience, not a deployment artefact). Use GOWORK=off to disable it in CI if needed.