Modules, Build & Tooling
Irbisa · cheatsheetSeptember 19, 2026

Modules, Build & Tooling

go.mod and MVS, build tags, embed, ldflags, linters, static binaries

Middle Developer16 itemscompressed for a skim
  1. 01

    A teammate resolves a merge conflict by deleting go.sum, and the next build stops with missing go.sum entry for module providing package .... Did deleting it change which versions the project builds?

    Easy

    No — go.sum is a checksum ledger, not a lock file: versions are chosen by the require lines in go.mod, and go.sum only records the hashes those versions are allowed to have.

    // go.mod — decides WHICH versions build
    module github.com/acme/billing
    
    go 1.23
    
    require (
    …
  2. 02

    The Makefile line go get github.com/golang/mock/mockgen stopped producing a binary when the team moved to Go 1.18, and go generate ./... now dies with mockgen: command not found. What replaced it?

    Easy

    go install <package>@<version> installs commands; go get only edits go.mod now.

    // ❌ 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
    …
  3. 03

    After go mod tidy your go.mod went 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

    // indirect means 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 own go.mod.

    // go.mod of a go 1.17+ module: two require blocks, two meanings
    module github.com/acme/billing
    
    go 1.23
    
    require (
    …
  4. 04

    go build ./... prints import cycle not allowed for internal/service -> internal/store -> internal/service. What is the idiomatic fix, and why does the compiler refuse instead of resolving it?

    Easy

    Declare the interface in the package that consumes itstore should depend on a small interface it defines itself, never import service back; 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
    …
  5. 05

    You run go get github.com/acme/kit@v1.4.0 and a colleague coming from npm asks why that did not also pull the newest golang.org/x/net, and where the lock file is. What rule is the go command following?

    Medium

    Minimal 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
    //
    …
  6. 06

    You tag v2.0.0 on your library and nobody gets it: go get -u keeps returning v1.9.3, and a consumer who hardcodes the tag is told the go.mod has a different module path. What did you skip?

    Medium

    Semantic 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
    …
  7. 07

    Your library works around a broken upstream with replace old => fork in its own go.mod, its tests pass, you tag it — and the first service that imports the library gets the broken upstream back. Why?

    Medium

    Only the main module's replace and exclude directives are applied — every such directive in a dependency's go.mod is 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.
    …
  8. 08

    CI on a Go 1.22 image happily builds a module whose go.mod says go 1.23.4, while a colleague sees go.mod requires go >= 1.23.4 (running go 1.22.5; GOTOOLCHAIN=local). What is the go directive actually doing?

    Medium

    The go line 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 unless GOTOOLCHAIN forbids 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
    …
  9. 09

    Adding an internal module git.corp.example/team/lib fails in CI with a 410 Gone from sum.golang.org and a proxy fetch that times out. Which setting is missing, and what would make the build work with no network at all?

    Medium

    GOPRIVATE=git.corp.example/* — it is the one knob that sets both GONOPROXY and GONOSUMDB for 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. 10

    A scanner flags github.com/foo/bits v0.0.0-20220331121500-1a2b3c4d5e6f in your build, yet no source file imports it and no hand-written require mentions it. How do you find who pulls it in, and what is that version string?

    Medium

    go mod why -m <module> prints the import chain that keeps it in the build, and v0.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. 11

    The files behind //go:build integration stopped 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?

    Medium

    A constrained file is not in the default build at all: go/build filters it out before the compiler ever sees it, so every tool that is not given the same -tags type-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. 12

    //go:embed web/dist is right there above the variable and the site works locally, but in the container every request for /_app/entry.js and /.vite/manifest.json returns file does not exist. What is being skipped?

    Medium

    go:embed silently skips files and directories whose name begins with . or _ unless the pattern is prefixed with all://go:embed all:web/dist embeds them.

    package assets
    
    import (
    	"embed"
    	"io/fs"
    	"net/http"
    …
  13. 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-ignored go.work. What is the mechanism, and how should the two modules be developed?

    Hard

    A workspace overrides the module graph locally: the use directives in go.work make those directories the modules being built, so the import resolved from a sibling checkout instead of a published version — and CI, with no go.work, resolves it the honest way and finds nothing in go.mod.

    // go.work at the repo root — git-ignored here, which is the whole bug
    go 1.23
    
    use (
    	./api
    	./lib
    …
  14. 14

    A binary cross-compiled on a macOS laptop runs fine in the golang image; copied into FROM scratch it fails on the first outbound HTTPS call with x509: certificate signed by unknown authority and on time.LoadLocation with unknown time zone. What is missing?

    Hard

    The image, not the binary — a static Go binary still reads the system CA bundle and the zoneinfo directory at runtime, and scratch has no files at all.

    package main
    
    import (
    	"net/http"
    	"time"
    …
  15. 15

    Two CI runs on the same commit produce binaries with different sha256, and release engineering also wants myapp version to print the commit. What makes the build byte-identical, and where does the version come from?

    Hard

    -trimpath plus a pinned toolchain — the usual difference is the absolute checkout path baked into the binary — and the version comes from -ldflags=-X or, for free, from the VCS stamps that runtime/debug.ReadBuildInfo() already carries.

    package main
    
    import (
    	"fmt"
    	"runtime/debug"
    )
    …
  16. 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?

    Hard

    govulncheck reports only vulnerabilities your code can actually reach, because it works from the static call graph rather than a version list — and the Printf mismatch is a go vet finding that go test already runs.

    package billing
    
    import (
    	"context"
    	"fmt"
    	"time"
    …