I have been carrying a Gopkg.toml around for years like a bad habit. dep did a reasonable job for its time, but its time was the awkward stretch before modules settled, and modules have been the obvious answer for long enough now that there is no excuse left. This week I finally converted a service that mattered, and the surprise was how little drama there was.
The mechanical part is almost nothing. From the repo root:
go mod init github.com/jmylchreest/thing
go mod tidy
go mod init reads the existing Gopkg.lock if it finds one and seeds go.mod with the pinned versions, which is the bit I had been quietly dreading and which turned out to be fine. go mod tidy then does the real work: it walks the import graph, adds anything missing, and drops anything you stopped using two refactors ago and never cleaned up. My first tidy removed four dependencies I genuinely did not remember pulling in.
The part that needs a human is the version resolution. dep and modules do not agree on how to choose versions. dep solves a constraint problem; modules use minimal version selection, which picks the lowest version that satisfies everything rather than the newest. So a couple of packages came across at older versions than I expected, and one came across at a commit pseudo-version like v0.0.0-20231104... because upstream had never tagged a release. That is not a bug, that is just the honest state of the dependency, made visible. dep had been papering over it.
Then the genuinely satisfying bit: deleting the vendor directory. I went back and forth on this. You can keep vendoring with modules, go mod vendor still exists, and for a while I told myself I would keep it for reproducible builds and air-gapped CI. But the module cache plus a GOFLAGS=-mod=readonly and a checksum database gives me the reproducibility I actually wanted, without committing eleven thousand files I never read. So out it went.
rm -rf vendor Gopkg.toml Gopkg.lock
go build ./...
go test ./...
Both passed first time, which felt like cheating after the years of friction this used to involve.
A few things worth knowing if you are about to do the same. Set GOFLAGS=-mod=readonly in CI so a stray import cannot silently rewrite go.mod during a build; you want that to be a loud failure, not a quiet diff. If you depend on a fork, use a replace directive rather than fighting the resolver, and leave a comment saying why, because future-you will not remember. And run go mod tidy as a CI check, not just a thing you do by hand, otherwise go.mod and go.sum drift out of sync with reality and you find out at the worst moment.
The honest summary is that I should have done this ages ago. dep was a fine bridge, but it was always a bridge, and standing on a bridge after the far side is built is just being in everyone's way. The toolchain is better, the failure modes are clearer, and my repo lost a directory the size of a small country. Good riddance to the lot of it.