« Back to Index

Go: Module Versioning

View original Gist on GitHub

Tags: #go #dependencies

Go Module Versioning.md

Reference: https://blog.golang.org/v2-go-modules

You have a dependency you want to use, like github.com/integralist/delete-just-testing.

// delete-just-testing/foo/foo.go 
//
package foo

import "fmt"

func Bar() {
	fmt.Println("bar")
}

That dependency has the following go.mod file:

module github.com/integralist/delete-just-testing

go 1.15

Note: Major version suffixes are not allowed at major versions v0 or v1 (e.g. you can’t do module github.com/integralist/delete-just-testing/v1). There is no need to change the module path between v0 and v1 because v0 versions are unstable and have no compatibility guarantee. Additionally, for most modules, v1 is backwards compatible with the last v0 version; a v1 version acts as a commitment to compatibility, rather than an indication of incompatible changes compared with v0. – official reference.

We want to create a new program, so we start by initializing a new go module for our project:

go mod init testing_gomodules

We create an app.go main package:

package main

import (
	"fmt"

	"github.com/integralist/delete-just-testing/foo"
)

func main() {
	fmt.Println("main")

	foo.Bar()
}

We execute go run app.go and find our go.mod file is updated:

$ go run app.go
go: finding module for package github.com/integralist/delete-just-testing/foo
go: downloading github.com/integralist/delete-just-testing v0.0.0-20200921145455-530f3130809d
go: found github.com/integralist/delete-just-testing/foo in github.com/integralist/delete-just-testing v0.0.0-20200921145455-530f3130809d
main
bar

The go.mod file for our project now looks like:

module testing_gomodules

go 1.15

require github.com/integralist/delete-just-testing v0.0.0-20200921145455-530f3130809d // indirect

Note: when I was running through this example I had pulled in the dependency into my project before I had actually assigned a v1.0.0 tag, so the go toolchain generates a pseudo-version for you. If I had tagged the commit with v1.0.0 before trying to use the dependency then instead of the pseudo-version I would have seen v1.0.0 after the module path.

If we (as the dependency author) want to update our code to be v2, then we have two strategies:

  1. create a new v2/ directory and copy our package into it (this is for backwards compatibility with go versions below 1.13 that don’t understand go modules).
  2. just rename the module in the go.mod file (which will require you to update all explicit imports in your code’s test files + consumers will need to do the same).

Note: go get -u doesn’t help consumers of our dependency get the latest major version. If you run go list -u -m all you’ll see minor and patch updates but not major version updates. This goes back to how the go team perceive v2 and higher major version releases (i.e. they should be a different directory or new module path).

Now let’s say we take the second approach of just changing the go.mod name in our dependency repository:

module github.com/integralist/delete-just-testing/v2 // << updated to append /v2

go 1.15

Then the consumer will need to update their import path to get that change:

package main

import (
	"fmt"

	"github.com/integralist/delete-just-testing/v2/foo" // << updated to include /v2
)

func main() {
	fmt.Println("main")

	foo.Bar()
}

…they’ll also discover they have an updated go.mod:

module testing_gomodules

go 1.15

require (
	github.com/integralist/delete-just-testing v0.0.0-20200921145455-530f3130809d
	github.com/integralist/delete-just-testing/v2 v2.0.0
)

You’ll see we have the tagged v2.0.0 pulled in alongside the original. Running go mod tidy will remove the old version.

Note: if you change the import path back to the non /v2 version you’ll discover the old v0.0.0... pseudo-version is put back into your go.mod file and you can again use go mod tidy to remove the v2.0.0 dependency. This is cool, because although the old pre-v2 code doesn’t exist any more at HEAD in the dependency’s master branch, the go toolchain is still able to retrieve it.

Easy module path update in Vim

Imagine you’re using a package called go-fastly and it’s currently defined at version 2.0.0 so it uses /v2 in its module path. But then an upgrade to 3.0.0 is released so you need to bump your imports from v2 to reference v3.

:Ack! --go 'go-fastly/v2'
:cdo s/v2/v3/ | update

If you don’t have Ack! because you’re using a basic vim configuration (e.g. vim -u ~/.vimrc-basic), then use the following instead…

:vimgrep /go\-fastly\/v2/j fastly/*go
:copen
:cdo s/v2/v3/ | update

NOTE: The reason to use vim -u ~/.vimrc-basic is because of vim-go and the golang LSP can cause the Vim quickfix window to get updated (various warnings/errors etc about the code) and this means :cdo fails to complete as the quickfix list it was working with has changed. So using a basic Vim configuration means I can use :cdo and have the files updated in a fraction of the time as no LSP means much faster processing.

Branch vs Directory Versioning

When creating a new module version most people use git branches to separate v0/v1 and v2,v3…etc and git tag appropriately when creating new releases.

An alternative is to use the same branch but separate directories for each version (this way you can more easily share code between the versions that haven’t actually changed).

  1. Directory Versioning Approach With directory versioning, instead of creating a new Git branch for each major version (v2, v3, etc.), you place the code for each major version in a dedicated folder within your repository. Each version has its own go.mod file and module path reflecting the correct version.

Here is a directory structure example:

├── v1
│   ├── go.mod
│   └── main.go
├── v2
│   ├── go.mod
│   └── main.go
└── v3
    ├── go.mod
    └── main.go

In this case, each folder (v1, v2, v3) contains the source code for its respective version, along with its own go.mod file specifying the correct module path.

The v1/go.mod would contain module github.com/your/module.

The v2/go.mod would contain module github.com/your/module/v2.

The v3/go.mod would contain module github.com/your/module/v3.

When users import and use your module, they must reference the correct version using the module path that corresponds to the major version they want.

// v0/v1
import "github.com/your/module"
// v2
import "github.com/your/module/v2"
// v3
import "github.com/your/module/v3"

Using go get or go install would then look like this…

# Install version 1
go get github.com/your/module@v1.5.0

# Install version 2
go get github.com/your/module/v2@v2.3.0

# Install version 3
go get github.com/your/module/v3@v3.0.0