Skip to content

Instantly share code, notes, and snippets.

@padamstx
Last active December 9, 2019 13:57
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save padamstx/b08ba3344871f13e791763bf41d1fbb3 to your computer and use it in GitHub Desktop.
Save padamstx/b08ba3344871f13e791763bf41d1fbb3 to your computer and use it in GitHub Desktop.
Example of versioning a module-aware Go library used by both module-aware and non-module-aware applications

Versioning a module-aware library written in Go

Overview

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.

Background

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.

Inventory

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's HelloWorld() function.

  • An application that uses "dep": https://github.com/padamstx/nomodapp
    This application is very similar to the modapp 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.

Reference(s)

I used this Go blog entry as my primary source of information while learning how to version Go modules.

Create and use first pre-release version of the library

Library - create gomodlib v0.0.1

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:

  1. In my "gomodlib" sandbox, I created the lib.go and lib_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.
  2. go mod init github.com/padamstx/gomodlib to create an initial version of the go.mod file.
  3. go test to verify that things are working correctly. This step also updates the go.mod file to include the "assert" dependency.
  4. go mod tidy
  5. git add .
  6. git commit -m "Create v0.0.1"
  7. git tag v0.0.1
  8. git push --follow-tags

These steps produced the v0.0.1 pre-release version of the library and made it available for applications.

Modapp - create first version which uses gomodlib v0.0.1

To create the initial version of the module-aware application (modapp), I followed these steps:

  1. 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.
  2. go mod init github.com/padamstx/modapp to create an initial version of the app's go.mod file.
  3. go install to build the app and install it in the $GOPATH/bin directory.
    Note: the go install command above found the v0.0.1 version of gomodlib and added a require statement to the go.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 the go.mod file to change the version number of gomodlib to be v0.0.1. The require statement in go.mod should look like this:
     require github.com/padamstx/gomodlib v0.0.1
  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
  1. go mod tidy
  2. git add .
  3. git commit -m "Create app"
  4. git push

These steps produced the initial version of the modapp application which uses version v0.0.1 of the library.

Nomodapp - create first version which uses gomodlib v0.0.1

To create the initial version of the non-module-aware application (nomodapp), I followed these steps:

  1. 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.
  2. dep init to create an initial Gopkg.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 the Gopkg.toml file. If you reproduce my steps now, it will likely use version 1.0.1 of the library instead, since that is the most recent version of the github.com/padamstx/gomodlib package. So, you will need to manually edit the Gopkg.toml file to use version 0.0.1 of the library for this initial version of the nomodapp application. The constraint in Gopkg.toml should look like this:
     [[constraint]]
       name = "github.com/padamstx/gomodlib"
       version = "0.0.1"
  1. dep ensure
  2. go install to build the app and install it in the $GOPATH/bin directory.
  3. 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
  1. git add .
  2. git commit -m "Create app"
  3. git push

These steps produced the initial version of the nomodapp application which uses version v0.0.1 of the library.

Create first production version (1.0.0) of library

Library - create gomodlib v1.0.0

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:

  1. go test to make sure everything works correctly
  2. go mod tidy (not absolutely required in this case, but a good habit nonetheless)
  3. git add .
  4. git commit -m "Create v1.0.0"
  5. git tag v1.0.0
  6. git push --follow-tags

Version "v1.0.0" of gomodlib is now available for use by applications.

Modapp - update to use gomodlib v1.0.0

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:

  1. go install to build and install the new version of the application.
  2. 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
  1. go mod tidy to keep the go.mod file clean.
  2. git add .
  3. git commit -m "Update lib dependency to v1.0.0"
  4. git push

The modapp application is now using version v1.0.0 of the library.

Nomodapp - update to use gomodlib v1.0.0

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:

  1. dep ensure to download version 1.0.0 of gomodlib and install it in the ./vendor directory.
  2. go install to build and install the new version of the application.
  3. 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
  1. git add .
  2. git commit -m "Update lib dependency to v1.0.0"
  3. 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!

Create new major version (v2.0.0) of library

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.

Library - create gomodlib v2.0.0

This commit contains the changes that were made to produce v2.0.0 of the library. Specifically, it includes the following:

  1. The entire package (including the go.mod and go.sum files) were moved to a new v2 directory.
  2. The (now renamed) v2/go.mod file was modified to reflect the new package name (github.com/padamstx/gomodlib/v2)
  3. lib.go was modified to change the version constant to be 2.0.0
  4. 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:

  1. 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.

  2. cd v2 && go mod tidy

  3. git add .

  4. git commit -m "Create v2.0.0"

  5. git tag v2.0.0

  6. git push --follow-tags

We now have a new major version of gomodlib available under a new package name github.com/padamstx/gomodlib/v2.

Modapp - update to use gomodlib v2.0.0

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:

  1. go install to build and install the new version of the application.
  2. 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
  1. go mod tidy to keep the go.mod file clean.
  2. git add .
  3. git commit -m "Update lib dependency to v2.0.0"
  4. git push

The modapp application is now using version v2.0.0 of the library.

Nomodapp - update to use gomodlib v2.0.0

This commit contains the change made to nomodapp to allow it to use gomodlib v2.0.0. Specifically, the following was done:

  1. 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. The Gopkg.toml file continues to use github.com/padamstx/gomodlib since it is actually used as the git repository URL, which does not include the "/v2" part.
  2. 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 the Gopkg.toml file.

Then I ran the following commands:

  1. dep ensure to download version 2.0.0 of gomodlib and install it in the ./vendor directory.
  2. go install to build and install the new version of the application.
  3. 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
  1. git add .
  2. git commit -m "Update lib dependency to v2.0.0"
  3. git push

The nomodapp application is now using version v2.0.0 of the library.

Applying maintainence to a previous major version 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:

  1. The lib.go and lib_test.go files were updated as in this commit.
  2. go test
  3. go mod tidy
  4. git add .
  5. git commit -m "Create v1.0.1"
  6. git tag v1.0.1
  7. git push --follow-tags

We now have version v1.0.1 of the library which can be used by applications.

Additional versioning steps

Version v2.1.0 of the library

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:

  1. git checkout master
  2. Modified v2/lib.go and v2/lib_test.go with the changes in this commit.
  3. cd v2 && go test
  4. cd v2 && go mod tidy
  5. git add .
  6. git commit -m "Create v2.1.0"
  7. git tag v2.1.0
  8. git push --follow-tags

Version v3.0.0 of the library

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:

  1. mv v2 v3 (renamed the top-level v2 directory to be v3 to reflect the new major version).
  2. I modified v3/lib.go and v3/lib.go to simulate a new feature.
  3. I modified v3/go.mod to update the package name to be:
   github.com/padamstx/gomodlib/v3
  1. cd v3 && go test
  2. cd v3 && go mod tidy
  3. git add .
  4. git commit -m "Create v3.0.0"
  5. git tag v3.0.0
  6. git push --follow-tags

This commit contains the changes that were pushed to the master branch for version v3.0.0.

Summary

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment