Modules, Build & Tooling
go.mod and MVS, build tags, embed, ldflags, linters, static binaries
- 01
A teammate resolves a merge conflict by deleting
go.sum, and the next build stops withmissing go.sum entry for module providing package .... Did deleting it change which versions the project builds?EasyNo —
go.sumis a checksum ledger, not a lock file: versions are chosen by therequirelines ingo.mod, andgo.sumonly records the hashes those versions are allowed to have.// go.mod — decides WHICH versions build module github.com/acme/billing go 1.23 require ( … - 02
The Makefile line
go get github.com/golang/mock/mockgenstopped producing a binary when the team moved to Go 1.18, andgo generate ./...now dies withmockgen: command not found. What replaced it?Easygo install <package>@<version>installs commands;go getonly editsgo.modnow.// ❌ Go 1.18+: this edits go.mod and installs nothing // $ go get github.com/golang/mock/mockgen // $ go generate ./... // mockgen: command not found // ✅ install a command at a pinned version, ignoring the current go.mod … - 03
After
go mod tidyyourgo.modwent from 6 requires to 40, most of them marked// indirect, and the reviewer wants to know whether the dependency set actually grew. What does the marker mean?Easy// indirectmeans no package in your module imports that module directly — it is listed because something you do import needs it, and since Go 1.17 those are written out explicitly instead of being read out of each dependency's owngo.mod.// go.mod of a go 1.17+ module: two require blocks, two meanings module github.com/acme/billing go 1.23 require ( … - 04
go build ./...printsimport cycle not allowedforinternal/service->internal/store->internal/service. What is the idiomatic fix, and why does the compiler refuse instead of resolving it?EasyDeclare the interface in the package that consumes it —
storeshould depend on a small interface it defines itself, never importserviceback; Go forbids package cycles because compilation and initialisation are strictly per package, in dependency order.// ❌ the cycle the compiler reports // package example.com/app/internal/service // imports example.com/app/internal/store from service.go // imports example.com/app/internal/service from store.go: import cycle not allowed // ✅ the consumer declares what it needs; the implementer imports nothing new … - 05
You run
go get github.com/acme/kit@v1.4.0and a colleague coming from npm asks why that did not also pull the newestgolang.org/x/net, and where the lock file is. What rule is the go command following?MediumMinimal Version Selection: the build uses the highest version that something in the graph explicitly requires, and never more — which is why Go needs no lock file and why adding one dependency does not re-resolve the others.
// the requirement graph (what each go.mod asks for) // acme/billing -> github.com/acme/kit v1.3.0 // acme/billing -> github.com/acme/api v0.9.0 // acme/kit v1.3.0 -> golang.org/x/net v0.28.0 // acme/api v0.9.0 -> golang.org/x/net v0.31.0 // … - 06
You tag
v2.0.0on your library and nobody gets it:go get -ukeeps returningv1.9.3, and a consumer who hardcodes the tag is told the go.mod has a different module path. What did you skip?MediumSemantic import versioning: from v2 on the major version is part of the module path (
module github.com/acme/lib/v2) and therefore part of every import path in every consumer.// ❌ tagged v2.0.0 while go.mod still says "module github.com/acme/lib" // -> consumers of the v1 path never see it; MVS treats it as a different module // -> a pinned fetch fails the path check, exactly like this real one: // // $ go get github.com/go-chi/chi@v5.0.12 // go: github.com/go-chi/chi@v5.0.12: invalid version: go.mod has post-v5 … - 07
Your library works around a broken upstream with
replace old => forkin its owngo.mod, its tests pass, you tag it — and the first service that imports the library gets the broken upstream back. Why?MediumOnly the main module's
replaceandexcludedirectives are applied — every such directive in a dependency'sgo.modis ignored, so yours stopped existing the moment your library stopped being the module being built.// ❌ inside the LIBRARY's own go.mod — green here, invisible to consumers // module github.com/acme/lib // require github.com/them/thing v1.4.0 // replace github.com/them/thing => github.com/acme/thing-fork v1.4.1-fix // // a service importing acme/lib resolves github.com/them/thing v1.4.0 — broken. … - 08
CI on a Go 1.22 image happily builds a module whose
go.modsaysgo 1.23.4, while a colleague seesgo.mod requires go >= 1.23.4 (running go 1.22.5; GOTOOLCHAIN=local). What is thegodirective actually doing?MediumThe
goline is both the language version and a hard minimum on the toolchain, and since Go 1.21 a toolchain that is too old will download and re-exec the required one unlessGOTOOLCHAINforbids it.// go.mod module github.com/acme/billing go 1.23.4 // language version AND the minimum toolchain toolchain go1.24.2 // which toolchain to actually run, when newer than the go line … - 09
Adding an internal module
git.corp.example/team/libfails in CI with a410 Gonefromsum.golang.organd a proxy fetch that times out. Which setting is missing, and what would make the build work with no network at all?MediumGOPRIVATE=git.corp.example/*— it is the one knob that sets bothGONOPROXYandGONOSUMDBfor those prefixes, so private paths are fetched straight from the VCS and never submitted to the public checksum database.// ❌ a private module routed through the public mirror and checksum DB // $ go get git.corp.example/team/lib@v1.2.0 // git.corp.example/team/lib: verifying module: git.corp.example/team/lib@v1.2.0: // reading https://sum.golang.org/lookup/...: 410 Gone // ✅ one variable covers GONOPROXY and GONOSUMDB for those prefixes … - 10
A scanner flags
github.com/foo/bits v0.0.0-20220331121500-1a2b3c4d5e6fin your build, yet no source file imports it and no hand-writtenrequirementions it. How do you find who pulls it in, and what is that version string?Mediumgo mod why -m <module>prints the import chain that keeps it in the build, andv0.0.0-<timestamp>-<commit>is a pseudo-version — a synthetic semver the go command mints for a commit that carries no tag.// who keeps it in the build — an import chain, or an explicit "not needed" // $ go mod why -m go.yaml.in/yaml/v3 // # go.yaml.in/yaml/v3 // github.com/acme/billing // github.com/acme/billing.test // github.com/stretchr/testify/require … - 11
The files behind
//go:build integrationstopped compiling three weeks ago and nobody noticed until the nightly job ran —go build ./...,go vet ./...and every editor stayed green. What is the mechanism, and what do you change?MediumA constrained file is not in the default build at all:
go/buildfilters it out before the compiler ever sees it, so every tool that is not given the same-tagstype-checks a different set of files than the nightly job does.// internal/store/pg_integration_test.go //go:build integration package store // Not compiled by `go build ./...`, not type-checked by `go vet ./...`, … - 12
//go:embed web/distis right there above the variable and the site works locally, but in the container every request for/_app/entry.jsand/.vite/manifest.jsonreturnsfile does not exist. What is being skipped?Mediumgo:embedsilently skips files and directories whose name begins with.or_unless the pattern is prefixed withall:—//go:embed all:web/distembeds them.package assets import ( "embed" "io/fs" "net/http" … - 13
The same commit builds on every developer's machine and fails in CI with
no required module provides package example.com/lib. The repo root has a git-ignoredgo.work. What is the mechanism, and how should the two modules be developed?HardA workspace overrides the module graph locally: the
usedirectives ingo.workmake those directories the modules being built, so the import resolved from a sibling checkout instead of a published version — and CI, with nogo.work, resolves it the honest way and finds nothing ingo.mod.// go.work at the repo root — git-ignored here, which is the whole bug go 1.23 use ( ./api ./lib … - 14
A binary cross-compiled on a macOS laptop runs fine in the
golangimage; copied intoFROM scratchit fails on the first outbound HTTPS call withx509: certificate signed by unknown authorityand ontime.LoadLocationwithunknown time zone. What is missing?HardThe image, not the binary — a static Go binary still reads the system CA bundle and the zoneinfo directory at runtime, and
scratchhas no files at all.package main import ( "net/http" "time" … - 15
Two CI runs on the same commit produce binaries with different
sha256, and release engineering also wantsmyapp versionto print the commit. What makes the build byte-identical, and where does the version come from?Hard-trimpathplus a pinned toolchain — the usual difference is the absolute checkout path baked into the binary — and the version comes from-ldflags=-Xor, for free, from the VCS stamps thatruntime/debug.ReadBuildInfo()already carries.package main import ( "fmt" "runtime/debug" ) … - 16
A dependency scanner opens thirty PRs a week for CVEs in transitive modules your code never calls, while a
fmt.Printf("%d", user.Name)shipped to production. How do you set the tooling up so the signal inverts?Hardgovulncheckreports only vulnerabilities your code can actually reach, because it works from the static call graph rather than a version list — and thePrintfmismatch is ago vetfinding thatgo testalready runs.package billing import ( "context" "fmt" "time" …