go.sum conflicts during merges are always my fault
Every team I’ve been on has at some point had a merge conflict in go.sum that someone tried to “resolve” by keeping both halves. The result is a broken lockfile, a subtly different dependency graph, and at least one sad afternoon. This post is me writing down the right way to handle it so I can send people a link instead of having the same conversation again.
First, what the files actually are:
go.moddescribes your module’s direct and some indirect dependencies, with minimum versions.go.sumcontains cryptographic hashes of the exact module versions thatgo.modresolves to, including all transitive dependencies. It’s a lockfile.
When two branches add or update dependencies independently, both files will often conflict. The correct way to resolve is NOT to manually merge the text. It’s to let the tooling regenerate:
# after resolving other conflicts, from the merge:
git checkout --theirs go.mod go.sum # or --ours, whichever
go mod tidy
git add go.mod go.sum
go mod tidy rereads your go.mod, walks your source code, and adjusts go.mod and go.sum to contain exactly what’s needed. It’s idempotent and safe to run.
If the two branches added different dependencies, you want BOTH sets. In that case:
# take your branch's version
git checkout --ours go.mod go.sum
# add the deps the other branch added by running tidy on a merge of the code
go mod tidy
# confirm the required versions are what you expect
go list -m all | grep somepackage
git add go.mod go.sum
The trick is that go mod tidy looks at your actual source code. If both branches added imports in code, after the source merge, the imports are present, and tidy figures out the versions.
A subtler case: both branches BUMPED the same dependency. go mod tidy will pick the minimum that satisfies all the require statements. Since both branches bumped, both require lines want a higher version. The merge resolution should pick the higher of the two (or the one you actually want), put it in go.mod, and re-tidy:
# after opening go.mod and picking the desired version for the conflicting line
go mod tidy
go build ./... # verify
git add go.mod go.sum
DO NOT ever:
- Manually edit
go.sum. The hashes are cryptographic and you cannot meaningfully edit them. - Accept both halves of a
go.sumconflict. You’ll get duplicate entries for a module version, whichgo mod verifywill reject, or worse, you’ll get conflicting hashes for the same version which is an instant-fail. - Delete
go.sumand let Go “regenerate it.” It will regenerate, but against whatever versions happen to be in module proxy caches right now, which might not be what your coworker had. Always regenerate viago mod tidy, not by deletion.
Some specific conflict shapes and what to do:
Shape 1: go.sum has 200 conflict markers. This happens when both branches upgraded a major transitive dependency. The fix is to forget the text merge:
git checkout HEAD -- go.sum # reset go.sum to your branch's version
go mod tidy # regenerate it
git add go.sum
Shape 2: go.mod has a conflicting version in a require line. Open go.mod, pick one version (probably the newer), delete the conflict markers, save, then:
go mod tidy
go build ./...
If go build fails, you picked a version that doesn’t have some API the code uses. Fix that, or pick the other version.
Shape 3: replace or exclude directives conflict. These are rare but real. Usually one branch added a replace for a local path (vendoring work in progress), the other branch added something else. Resolve manually in go.mod, then go mod tidy.
A couple of prevention tips that have helped my teams:
go mod tidyin CI. Fail the build ifgo mod tidywould change anything. Put it in a pre-merge check. This prevents the “lockfile was never regenerated after a dependency change” class of bug from reaching main.
go mod tidy
if ! git diff --quiet go.mod go.sum; then
echo "go.mod or go.sum out of date. Run 'go mod tidy'."
exit 1
fi
Don’t commit IDE-added imports you don’t use. Some IDEs will auto-import packages on autocomplete, which can cause
go mod tidyto ADD dependencies you didn’t intend.Use
go mod downloadin yourDockerfilebefore copying source. This caches dependencies in a Docker layer. Any change togo.mod/go.suminvalidates the layer, but code-only changes don’t.Pinning with
replaceis fine, but leave a comment. If you’ve gotreplace example.com/foo => ../foobecause you’re iterating on a local fork, leave a// TODO: remove once v1.2.3 releasedcomment so it doesn’t live forever.
Most go.sum conflicts are annoying but quick. The bad cases are when someone resolved a conflict by accepting both halves and pushed to main, because then the whole team gets a broken checkout and it takes a while to figure out why go build is complaining about hash mismatches. Build the CI check. Teach your team to use go mod tidy. The conflicts get boring, and that’s what you want.