Dependency Management
Modules, go.mod and go.sum in place of Maven or Gradle: adding dependencies, major versions, local development and private repositories.
Section 02 of 31 6 min read
Every Java project starts with a pom.xml or build.gradle and a build tool to interpret it. Go has one built-in module system driven by the go command itself: no plugins, no wrapper scripts, no build language.
Initialising a module
# Maven archetype
mvn archetype:generate \
-DgroupId=com.mycompany \
-DartifactId=my-app \
-DarchetypeArtifactId=maven-archetype-quickstart
# Gradle
gradle init --type java-application# One command, one file
go mod init github.com/mycompany/my-app
# go.mod now contains:
# module github.com/mycompany/my-app
#
# go 1.27.1go.mod and go.sum
<project>
<groupId>com.mycompany</groupId>
<artifactId>my-app</artifactId>
<version>1.0.0</version>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.19.0</version>
</dependency>
</dependencies>
</project>module github.com/mycompany/my-app
go 1.26.0
require (
github.com/jackc/pgx/v5 v5.7.5
golang.org/x/sync v0.16.0
)
require (
// Pulled in by the modules above
github.com/jackc/puddle/v2 v2.2.2 // indirect
)| Java | Go |
|---|---|
pom.xml / build.gradle | go.mod |
gradle.lockfile (opt-in) | go.sum: always present, commit it |
~/.m2/repository | The module cache: go env GOMODCACHE |
mvnw / gradlew | The go command; a toolchain line can pin the exact Go version |
mvn package / gradle build | go build ./... |
mvn dependency:resolve | go mod download |
mvn dependency:tree | go mod graph or go list -m all |
mvn dependency:analyze | go mod tidy: adds missing and removes unused requirements |
Minimal version selection
Maven resolves conflicts with "nearest definition wins", which can silently pick an older version than a library asked for. Go uses minimal version selection: for each module, the build uses the highest version that any require line in the graph asks for, and never anything newer. The result is deterministic, needs no lock-file solver, and changes only when a go.mod file changes.
Maven: nearest definition wins
Go: minimal version selection
require line asks for, and never anything newer.Adding and removing dependencies
# Add or upgrade a dependency (updates go.mod and go.sum)
go get github.com/jackc/pgx/v5@v5.7.5
go get github.com/jackc/pgx/v5@latest
# Upgrade all direct and indirect dependencies (minor/patch)
go get -u ./...
# Add missing requirements, drop unused ones
go mod tidy
# Pre-fetch everything into the module cache (useful in CI images)
go mod download
# Why is this module in my build?
go mod why -m github.com/jackc/puddle/v2
# Which dependencies have newer versions?
go list -m -u allSemantic import versioning
Go has a rule with no Maven or Gradle equivalent: a module that releases a breaking v2 or later changes its module path to end in /v2. The major version is part of the import path, so two major versions are simply two different packages.
// go.mod
require (
github.com/some/lib v1.5.0 // v0/v1: no suffix
github.com/some/lib/v2 v2.1.0 // v2+: suffix in the path
)
// Each major version is a separate import path
import (
libv1 "github.com/some/lib"
libv2 "github.com/some/lib/v2"
)
// Both compile into the same binary without clashing.
// In Java, two versions of one artifact on the classpath
// means shading or a runtime NoSuchMethodError.Working on several modules at once
# Build and install the library locally
cd ../shared-lib
mvn install # publishes 1.0-SNAPSHOT to ~/.m2
# Then depend on the SNAPSHOT version
# from the application's pom.xml# From a parent directory holding both repos
go work init ./my-app ./shared-lib
# go.work now says: build my-app against the
# local shared-lib checkout, not the published one.
# Keep go.work out of version control.
cd my-app && go build ./...The older alternative is a replace directive in go.mod, for example replace github.com/mycompany/shared-lib => ../shared-lib. It works, but it is easy to commit by accident. go.work (Go 1.18+) is the better tool for local multi-module development because it lives outside any module.
Pinning tools with the tool directive (Go 1.24+)
Code generators and linters need pinned versions just like libraries. Before Go 1.24 the idiom was a tools.go file with a //go:build tools constraint and blank imports. Go 1.24 added a tool directive to go.mod.
<build>
<plugins>
<plugin>
<groupId>org.openapitools</groupId>
<artifactId>openapi-generator-maven-plugin</artifactId>
<version>7.14.0</version>
</plugin>
</plugins>
</build>// Add a tool: records it in go.mod and go.sum
// go get -tool github.com/sqlc-dev/sqlc/cmd/sqlc@latest
// go.mod then contains
tool github.com/sqlc-dev/sqlc/cmd/sqlc
// plus a matching require line. Run it with
// go tool sqlc generate
// Everyone on the team gets the same version.Private modules
# Modules matching GOPRIVATE bypass the public proxy
# (proxy.golang.org) and the public checksum database.
go env -w GOPRIVATE='github.com/mycompany/*'
# GOPRIVATE is the default for both GONOPROXY and GONOSUMDB;
# set those separately only if you run a private proxy.
# Authentication is plain git. For example, force SSH:
git config --global url."git@github.com:".insteadOf "https://github.com/"