Go Module’s Replace
Tags: Replace, Go, Go Modules

Photo by SK Yeong on Unsplash

I believe in keeping software dependencies up to date in a timely fashion. I’ve never liked the if it ain't broke, don't update it approach. I’ve written about this before. With Go projects I periodically run:

$ go get -u the/module/name

To update the dependencies in a project using the/module/name of the current project. Usually, this just grinds against the go.mod file and does the right thing, updating all the modules and their dependencies to the latest and greatest. Sometimes, when you run this, if a dependency introduced a breaking change, your code won’t build, or your tests fail. At that point you just do the needful, fixing your code, and you are done.

Occasionally, however, you hit the following scenario. You depend on module ONE, which depends on module X. You also depend on module TWO, and that also depends on module X (module X is referred to as a transitive dependency). In this scenario, X introduced a breaking change, which module ONE responded to, but module TWO has ignored. You are caught in the middle of this, you want the newest stuff, but the newest ONE wants the newest X, but the newest TWO wants an older, incompatible X. Until TWO updates you cant move ONE forward.

What do you do? Well, you can wait and hope TWO catches up, maybe asking them politely to do so, or you can try and fix things yourself.

The Good Citizen Solution

You can be a good citizen and do the following:

Fork repo TWO Run go get -u TWO and deal with the breaking changes. Commit the update to your fork. Submit a Pull Request at TWO against your fork Ask them to accept the update Wait for the Pull Request to be merged

Hopefully, they are thrilled that someone helped them out, merge your changes and you can now update your original project and everything will work.

If They Don't Merge The Change?

If the Pull Request isn’t merged Go has a solution for that scenario. You can replace the original TWO reference with your fork in your go.mod!

A Practical Example

I’ve a project snipgo, that used the github.com/gdamore/tcell, and github.com/pgavlin/femto:

module github.com/nwillc/snipgo



go 1.15



require (

...

github.com/gdamore/tcell v1.4.0

...

github.com/pgavlin/femto v0.0.0-20191028012355-31a9964a50b5

...

)

Additionally, github.com/pgavlin/femto also used tcell, so it’s also a transitive dependency there.

When I ran:

go get -u github.com/nwillc/snipgo

The tcell module updated, in a breaking way which I could easily deal with, but the femto module had not updated yet. I followed the six steps above and…. [sound of crickets] they didn’t respond to my Pull Request.

Go Module Replace To The Rescue!

I didn’t want to be left waiting, so here’s the solution. In my forked repo I did:

go list all -m github.com/nwillc/femto

That command showed me detailed version information of my fork. Next I edited my original project’s go.mod adding a replace at the end with that version info:

replace github.com/pgavlin/femto => github.com/nwillc/femto v0.0.0-20201217031030-474e70183a86

That “replaced” the github.com/pgavlin/femto with my fork github.com/nwillc/femto. And finally, back in snipgo I tried again:

go get -u github.com/nwillc/snipgo

Viola! Now both tcell and femto, specifically the femto I replaced with my fork, both were on the same version and everything else sorts itself out!

Why Bother?

Seems excessive? Why bother? This route has a couple of major advantages:

Your Good Citizen efforts might still pay off and they might merge your work. Until they do, the replace tactic allows you to leave your code’s imports unchanged and also allows you to push the changes to your project’s repo in a way that will be transparent to people using your repo. When and if the Pull Request merges, you can remove the replace with ease.

Update

The femto repo did eventually accept my PR and I just removed the replace and the project was all good using the original repo.