Module 2 · Lesson 3 · ~20 min read

Modules, Vendoring, and Dependencies

A Go module is the unit of versioning, distribution, and dependency. Two files run the show: go.mod and go.sum. Get those right and most dependency questions answer themselves.

What a module is

A module is a directory tree containing a go.mod file at the root. The go.mod declares:

// go.mod
module github.com/example/cantonctl

go 1.22

require (
    google.golang.org/grpc v1.60.0
    google.golang.org/protobuf v1.32.0
    github.com/spf13/cobra v1.8.0
)

require (
    // indirect — pulled in transitively, not directly imported by this module
    golang.org/x/net v0.20.0 // indirect
)

The commands you'll run constantly

CommandWhat it does
go mod init <path>Create a new module in the current directory.
go mod tidyAdd missing imports to go.mod, remove unused ones, normalize.
go get <module>@<version>Add or upgrade a specific dependency.
go get -u ./...Upgrade all direct dependencies to latest minor/patch.
go build ./...Build everything in the module.
go test ./...Test everything.
go mod why <module>Explain why a particular module is in your dep graph.
go mod graphPrint the full dep graph (machine-readable).
go mod vendorCopy all deps into a vendor/ directory for hermetic builds.

go.sum — the lockfile

For every direct and indirect dependency, go.sum records cryptographic hashes of the downloaded module content. The Go toolchain verifies these on every build. Commit go.sum. It is your supply-chain integrity guarantee.

# Excerpt from a real go.sum
google.golang.org/grpc v1.60.0 h1:6+QweTERNxz3HQECXq9CqGXxUWxSmGPyyW+vMltnpzs=
google.golang.org/grpc v1.60.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=

If a downloaded module's hash doesn't match what's in go.sum, the build fails. This catches both silent registry tampering and accidental typo-squatting.

Versioning — semver, with a twist

Go modules use semver: v1.2.3 = MAJOR.MINOR.PATCH. The major version above 1 is part of the import path:

import "github.com/foo/bar"          // v0 or v1
import "github.com/foo/bar/v2"       // v2.x.x
import "github.com/foo/bar/v3"       // v3.x.x

This is "semantic import versioning." It guarantees that two major versions of a module can coexist in your dep graph without conflict — useful when a transitive dep upgrades but you can't.

v0.x.y versions are explicitly "no compatibility guarantees." A surprising number of widely-used Go libraries live perpetually in v0 — the Go community is comfortable with that. The Canton ecosystem will likely include some.

Replace — the local-development escape hatch

// In go.mod, while developing locally:
replace github.com/example/canton-protos => ../canton-protos

Forces the named dependency to come from a local path instead of the registry. Useful when you're co-developing two modules. Remove before committing — replace directives are toxic in production.

Vendoring

Run go mod vendor and you get a vendor/ directory containing every dependency's source. Subsequent builds use the vendored copy instead of the module cache.

When to vendor:

When not: small projects, libraries (vendor in apps, not libs), public open-source where the cache works fine.

The module proxy

By default, Go fetches modules through proxy.golang.org — a Google-run cache that:

For private modules: set GOPRIVATE=github.com/your-org/* to skip the proxy and sumdb for those paths. Required when working with a closed-source Canton-related repo.

Multi-module repos and workspaces

If you have several modules in one repo, go.work at the top level lets the toolchain treat them as a unit during local development.

// go.work
go 1.22

use (
    ./client
    ./server
    ./shared
)

Inside a workspace, dependencies between local modules are resolved locally — no replace directives, no path gymnastics. Workspaces are a development-time tool; they don't ship to production.

Common gotchas

Forgetting go mod tidy

You add an import, build works locally because the cache has it, but CI fails because go.mod wasn't updated. Run tidy before every commit. Many teams have a pre-commit hook for it.

Mixed go.mod and GOPATH

Pre-modules Go used $GOPATH/src. Modern Go uses $GOMODCACHE (default ~/go/pkg/mod). If a tutorial says "put your code under $GOPATH/src/github.com/...", it's pre-2018 and out of date. Ignore.

Major version creep

If you import github.com/foo/bar and the maintainer cuts v2, your code does not automatically upgrade. v2 is at a different import path. go list -m -u all shows what's available; go get github.com/foo/bar/v2 moves you forward.

Takeaways