This gist provides an example of how to version a library written in Go which uses Go modules for its dependency management. Included in this example are two applications - one that uses Go modules, and one that uses "dep" for dependency management.
I recently migrated an existing Go-based library from using "dep" to using Go modules for its dependency management. At the time I did the migration, the library was at version 1.0.0, and I delivered the "module-aware" version as v1.0.1. When this library (module) eventually progressed to v2.0.0, I was not yet aware of the module-related requirements associated with version 2 and beyond of a Go module. This shortcoming was reported as an issue against the library, which prompted me to do more research on Go modules. After learning more about Go modules, in order to verify my plans for fixing the library, I put together a small experiment consisting of three git repositories that allowed me to simulate the versioning steps of a mythical library, and two applications that use the library. This gist is an explanation of that experiment.
There are three git repositories associated with this experiment:
-
The library: https://github.com/padamstx/gomodlib
This library provides a single function -HelloWorld()
- which returns a string message that contains the library's version number. -
An application that uses Go modules: https://github.com/padamstx/modapp
This application simply prints a message which includes the message text returned by the library'sHelloWorld()
function. -
An application that uses "dep": https://github.com/padamstx/nomodapp
This application is very similar to themodapp
application above, except that it uses "dep" for dependency management. In my case, my real library will need to be usable by both module-aware and non-module-aware applications, so it was important for me to include both types of applications in my experiment.
Throughout this narrative, I'll include links to specific commits within these repositories to illustrate the types of changes that were made at each step.
I used this Go blog entry as my primary source of information while learning how to version Go modules.
My first step was to create a pre-release version (v0.0.1) of the library.
Specifically, I created a lib.go file which contains the HelloWorld()
function, along with
a lib_test.go file which verifies it, plus the go.mod
and go.sum
files.
Specific steps:
- In my "gomodlib" sandbox, I created the
lib.go
andlib_test.go
files as seen in this commit.
Note: because this library uses Go modules, the location of the sandbox must not be located within $GOPATH. In my environment, GOPATH is set to~/go
, so I located this sandbox in a directory that is not underneath~/go
. go mod init github.com/padamstx/gomodlib
to create an initial version of thego.mod
file.go test
to verify that things are working correctly. This step also updates thego.mod
file to include the "assert" dependency.go mod tidy
git add .
git commit -m "Create v0.0.1"
git tag v0.0.1
git push --follow-tags
These steps produced the v0.0.1 pre-release version of the library and made it available for applications.
To create the initial version of the module-aware application (modapp), I followed these steps:
- In my "modapp" sandbox, I created the main.go file as seen in this commit.
Note: because this application uses Go modules, the location of the sandbox must not be located within $GOPATH. In my environment, GOPATH is set to~/go
, so I located this sandbox in a directory that is not underneath~/go
. go mod init github.com/padamstx/modapp
to create an initial version of the app'sgo.mod
file.go install
to build the app and install it in the$GOPATH/bin
directory.
Note: thego install
command above found the v0.0.1 version ofgomodlib
and added arequire
statement to thego.mod
file for me. If you reproduce my steps now, you will get slightly different results because more versions of the library are now available. So, you will need to manually edit thego.mod
file to change the version number ofgomodlib
to bev0.0.1
. Therequire
statement ingo.mod
should look like this:
require github.com/padamstx/gomodlib v0.0.1
modapp
to run the application and verify that it is using version 0.0.1 of the library:
$ modapp
modapp main, version 1.0.0
message from gomodlib: Hello, world. This is version: 0.0.1
go mod tidy
git add .
git commit -m "Create app"
git push
These steps produced the initial version of the modapp
application which uses version v0.0.1 of the library.
To create the initial version of the non-module-aware application (nomodapp), I followed these steps:
- In my "nomodapp" sandbox, I created the main.go file as seen in this commit.
Note: because this application does not use Go modules, the location of the sandbox must be located within $GOPATH. In my environment, GOPATH is set to
~/go
, so I located my sandbox in~/go/src/github.com/padamstx/nomodapp
. dep init
to create an initialGopkg.toml
file. Note: when I ran this command, only v0.0.1 of the library existed, so that's the version that was inserted into theGopkg.toml
file. If you reproduce my steps now, it will likely use version1.0.1
of the library instead, since that is the most recent version of thegithub.com/padamstx/gomodlib
package. So, you will need to manually edit theGopkg.toml
file to use version0.0.1
of the library for this initial version of thenomodapp
application. The constraint inGopkg.toml
should look like this:
[[constraint]]
name = "github.com/padamstx/gomodlib"
version = "0.0.1"
dep ensure
go install
to build the app and install it in the$GOPATH/bin
directory.nomodapp
to run the application and verify that it is using version 0.0.1 of the library:
$ nomodapp
nomodapp main, version 1.0.0
message from gomodlib: Hello, world. This is version: 0.0.1
git add .
git commit -m "Create app"
git push
These steps produced the initial version of the nomodapp
application which uses version v0.0.1 of the library.
This commit contains
the changes made to the library to consititute version 1.0.0. In the case of this experiment, all I really
did was modify the version
constant in lib.go along with the corresponding change to the testcase.
After making the changes to the lib.go and lib_test.go files, I ran the following commands:
go test
to make sure everything works correctlygo mod tidy
(not absolutely required in this case, but a good habit nonetheless)git add .
git commit -m "Create v1.0.0"
git tag v1.0.0
git push --follow-tags
Version "v1.0.0" of gomodlib is now available for use by applications.
This commit contains
the changes made to modapp
to allow it to use gomodlib v1.0.0. Specifically, I modified
the go.mod
file to reference version v1.0.0
instead of v0.0.1
, then I ran the following commands:
go install
to build and install the new version of the application.modapp
to verify that we're now using version v1.0.0 of gomodlib:
$ modapp
modapp main, version 1.0.0
message from gomodlib: Hello, world. This is version: 1.0.0
go mod tidy
to keep thego.mod
file clean.git add .
git commit -m "Update lib dependency to v1.0.0"
git push
The modapp
application is now using version v1.0.0 of the library.
This commit contains
the changes made to nomodapp
to allow it to use gomodlib v1.0.0. Specifically, I modified
the Gopkg.toml
file to reference version 1.0.0
instead of 0.0.1
, then I ran the following commands:
dep ensure
to download version 1.0.0 of gomodlib and install it in the./vendor
directory.go install
to build and install the new version of the application.nomodapp
to verify that we're now using version v1.0.0 of gomodlib:
$ nomodapp
nomodapp main, version 1.0.0
message from gomodlib: Hello, world. This is version: 1.0.0
git add .
git commit -m "Update lib dependency to v1.0.0"
git push
The nomodapp
application is now using version v1.0.0 of the library.
So far, nothing surprising or spectacular, but things are about to get real!
Suppose we now need to make a breaking change to the gomodlib
library.
Since most (all?) go packages use semantic versioning, this means that we'll need to bump up the version
of the library from 1.0.0
to 2.0.0
. When you see the minor change I made to create version 2 of the library, don't get
confused as this is only an experiment. Normally a package would have its major version number incremented only when a
true breaking change is being made to its API, but in this experiement we're just simulating the breaking change.
Because gomodlib
is using Go modules we actually need to modify the package name to reflect the new major version.
For this reason, the package name will need to change to github.com/padamstx/gomodlib/v2
.
This commit contains the changes that were made to produce v2.0.0 of the library. Specifically, it includes the following:
- The entire package (including the
go.mod
andgo.sum
files) were moved to a newv2
directory. - The (now renamed)
v2/go.mod
file was modified to reflect the new package name (github.com/padamstx/gomodlib/v2
) lib.go
was modified to change theversion
constant to be2.0.0
lib_test.go
was modified to check for the new version number as well.
After these changes to the working tree were made, I ran the following commands:
-
cd v2 && go test
Note that we now need to execute all "go" commands in the new "v2" directory since that's where we moved our package contents. -
cd v2 && go mod tidy
-
git add .
-
git commit -m "Create v2.0.0"
-
git tag v2.0.0
-
git push --follow-tags
We now have a new major version of gomodlib
available under a new package name github.com/padamstx/gomodlib/v2
.
This commit
contains the changes made to modapp
to allow it to use gomodlib v2.0.0. Specifically, I modified
the go.mod
file to reference version v2.0.0
of gomodlib
and I added "/v2" to the package name.
In addition, I modified main.go
to use the new import path github.com/padamstx/gomodlib/v2
.
Then I ran the following commands:
go install
to build and install the new version of the application.modapp
to verify that we're now using version v2.0.0 of gomodlib:
$ modapp
modapp main, version 1.0.1
message from gomodlib: Hello, world. This is version: 2.0.0
go mod tidy
to keep thego.mod
file clean.git add .
git commit -m "Update lib dependency to v2.0.0"
git push
The modapp
application is now using version v2.0.0 of the library.
This commit
contains the change made to nomodapp
to allow it to use gomodlib v2.0.0.
Specifically, the following was done:
- The
Gopkg.toml
file was updated to reflect version 2.0.0 of the gomodlib module. Notice, however, that we did NOT add "/v2" to the end of the package name. TheGopkg.toml
file continues to usegithub.com/padamstx/gomodlib
since it is actually used as the git repository URL, which does not include the "/v2" part. - The
main.go
file was modified to add "/v2" to the end of the import path because the import statement needs the actual package name, not the git URL as in the case of theGopkg.toml
file.
Then I ran the following commands:
dep ensure
to download version 2.0.0 of gomodlib and install it in the./vendor
directory.go install
to build and install the new version of the application.nomodapp
to verify that we're now using version v2.0.0 of gomodlib:
$ nomodapp
nomodapp main, version 1.0.1
message from gomodlib: Hello, world. This is version: 2.0.0
git add .
git commit -m "Update lib dependency to v2.0.0"
git push
The nomodapp
application is now using version v2.0.0 of the library.
At this point, we've delivered versions v0.0.1, v1.0.0 and v2.0.0 of the gomodlib
library, all using the
master branch of the git repository. We haven't needed any other branches for maintaining the code because
all our development has been sequential with one version (commit) building on the previous one.
But this is about to change!
Let's suppose that a user of gomodlib
has reported a bug in the v1.0.0 version.
In this case, we'll want to fix the bug by modifying the v1.0.0 source code of the library, resulting in a
new version v1.0.1 (in semantic versioning, we increment the patch level to indicate a bug fix).
In our case, v1.0.0 represents the very latest (and only) version within the v1 major
version, but if there were other minor versions or patch levels present (e.g. v1.1.0, v1.3.4, etc.), we would
probably want to select the very latest version within the "v1" major version as the starting point for fixing
the bug.
In my case, I chose to use a fairly light-weight branching strategy to handle support requirements like this. Before fixing the bug, I'll create a new "release" branch, then push the resulting commit to that branch. The name of the release branch will be "v1-release", and would be used from this point forward to fix any bugs found in the v1 major version. Other branching strategies might involve the creation of a release branch whenever a new major version (release) of the library is tagged, but my strategy I defer creating the release branch until it's actually needed.
To create the release branch, I used this command:
git checkout -b v1-release v1.0.0
This creates the v1-release
branch off the commit associated with the v1.0.0
tag.
Once this branch is created, I simulated the bug fix by creating version v1.0.1 of the library using these steps:
- The
lib.go
andlib_test.go
files were updated as in this commit. go test
go mod tidy
git add .
git commit -m "Create v1.0.1"
git tag v1.0.1
git push --follow-tags
We now have version v1.0.1 of the library which can be used by applications.
After creating gomodlib
v1.0.1, I resumed new development work by creating version v2.1.0, which
simulates the addition of a new (compatible) feature without breaking the v2 API.
This change was pushed to the master branch which continues to serve as our tip of new development.
The commands that I used to create version v2.1.0 were:
git checkout master
- Modified
v2/lib.go
andv2/lib_test.go
with the changes in this commit. cd v2 && go test
cd v2 && go mod tidy
git add .
git commit -m "Create v2.1.0"
git tag v2.1.0
git push --follow-tags
After creating version v2.1.0, I also simulated the creation of a new major version, v3.0.0. To do this, I followed the steps similar to those used to create version v2.0.0:
mv v2 v3
(renamed the top-level v2 directory to be v3 to reflect the new major version).- I modified
v3/lib.go
andv3/lib.go
to simulate a new feature. - I modified
v3/go.mod
to update the package name to be:
github.com/padamstx/gomodlib/v3
cd v3 && go test
cd v3 && go mod tidy
git add .
git commit -m "Create v3.0.0"
git tag v3.0.0
git push --follow-tags
This commit contains the changes that were pushed to the master branch for version v3.0.0.
In this gist, I've illustrated how to take a module-aware Go package through various development stages to produce multiple versions. The primary goal of the experiment was to produce multiple major versions of the library and ensure that applications can easily consume those versions.