How to use a Fork with Go/GoLang

Hagen Hübel
5 min readApr 10, 2020

Although I’ve already spent plenty of days, weeks and even months with GoLang building several REST-API’s, a Web-Application and various Libraries, today was the first time I had to fork a package and use this fork immediately instead of waiting for getting a Pull Request merged and released.

If you are at the same level of knowledge like me as a Go developer, this post will probably save you tons of hours!

Since I am familiar very well with dependency management for 2 decades across a bunch of languages and frameworks like CocoaPods, Composer, NPM, Yarn, NuGet and recently with the package management in Flutter, I was quiet naiv and thought: alright, let’s just fork it, then modify the code, publish it and use the fork in my application as it has been always very easy so far in any other environment besides Go.

Holy shit! That was anything but easy! And unexpected things happened!

Here is what I wanted to do, what I did, in which problems I ran into and what the final solution was.

*** If you are only interested in the solution, press page-down as much as you can. ***

1. The Fork

The package I had to fork was https://github.com/blang/semver. I came across this package when I was dealing earlier today with semantic version numbers in order to automate our deployment:

  • read the current version out of a file called “VERSION”
  • be able to increment the major, minor or patch version.
  • write back the new version string into a file called “VERSION“

(The reason for this is that we use this VERSION file as the “variable” that contains the current version of an API and will be used in the auto-generation of Swagger-Files, docker-images, release-tags and some other special purposes)

When I suddenly found myself building it all by myself and reinventing the wheel once again, I started to use Google in order to find an already existing package for this semver-related task. I found https://github.com/blang/semver that will provide anything I wanted to have here for convenience. However, this package is sort of opinionated and disallows the manipulation of versions having a Major of Zero: incrementing major, minor or patch of a version string will throw an error, if the major is zero like “0.1.2”.

I forked the library, removed these error throwing lines and fixed also the tests in order to get ready with the new rules. Although I prefer to contribute to Open Source projects and frequently open Pull requests in case of found issues (and while these pieces mostly get merged), I figured out very slim odds in this case to get something like this merged in soon. Therefor I pushed into master on my own fork instead of a feature branch and made a new release with a version number of v3.7.0, based on current version number v3.6.1 in the original package:

git tag -a v3.7.0 -m “v3.7.0”
git push --tags

From now on, things went odd!

2. Using the fork instead of the original package

First, I started to remove the following line

github.com/blang/semver

from the file go.mod.

Working with IntelliJ or GoLand means to close these applications and change such files with a tool like VIM because these applications always try to fix your mistakes and indicate something like this as a mistake and will “repair” it back to the previous state!

I also removed every single line in `go.sum` that was pointing to a version of semver. For reasons that are unknown to me, there were plenty of lines pointing to the semver-package in various versions.

Then I removed the import-statement with the “old” repository and replaced it with the right GitHub-URI to the new one (my fork):

import "github.com/itinance/semver"

This basically would mean: don’t use the master of the original package, but please use the latest release of my own fork.

Sounds reasonable, isn’t it? And this would be the right approach, what I would expect so far, but shit happened!

The Go-compiler was running wild and presented me this message.

~/go/src/github.com/xxxxx/xxxxx/go.mod:11: require github.com/itinance/semver: version “v3.7.0” invalid: module contains a go.mod file, so major version must be compatible: should be v0 or v1, not v3

I started to “google” this message, especially the latest part starting after “invalid:” and came across a bunch of similar questions, but mostly in different cases and scenarios.

Even when I did a separate

go get github.com/itinance/semver

it checked out an older version (sometimes it was v1.1, sometimes it was v2.2), both that I have never published nor I didn’t knew this library back then when v2.2 was published!

I removed everything in the `${GOPATH}/pkg` directory and tried to “go get” again. But it kept with version v2.2, which was dead old and had nothing to do with my fork.

I could even try to

go get github.com/itinance/semver@v3.7.2

or

go get github.com/itinance/semver@master

and got many versions delivered, but no one that was submitted after 2019.

3. The solution

It took me another hour of investigation, and Peter Bourgon (https://twitter.com/peterbourgon) helped out a lot here to point things out, the final steps that led to the solution were:

3.1. The forked repository needs to be imported as

import "github.com/itinance/semver/v3"

The “v3”-suffix here is based on the final version-tag/release-tag “v3.7.2”.

3.2. Therefor the Fork needs to have the following go.mod:

module github.com/itinance/semver/v3

go 1.14

3.3. MOST IMPORTANT: PUSH THESE CHANGES INTO THE v3-TAG!!!

Nevertheless, the most important step is this:

In order to get found by “go get/go mod” and what ever else you might use for dependency management:

It’s not enough just to push your latest changes into master, they have to be pushed into a new tag that was prefixed with “v3”, like

git tag -a v3.7.2 -m “v3.7.2”
git push --tags

although they are already plenty of other tags prefixed with v3 available. Go seems to take special care of these tags in git in order to resolve a version constraint!

Final Thoughts

My first thought was: holy shit, what are you doing here, Google?

But at the end I came to the conclusion that this would be the most robust handling of dependencies and their versioning that I have ever seen so far.

--

--