Go package: a brief tour into the world of Go Module

enbis

enbis

Posted on April 26, 2020

Go package: a brief tour into the world of Go Module

Starting from Go 1.11, the Golang team decided to introduce a new dependency management system: the Modules.

A Module is a collection of Go Packages with explicit version information. The go.mod file is used to store that information and it is located at the root of the project tree. The idea behind this concept is similar to what npm is for Node.

In this post, we will see how the main project interacts with the go.mod file and how it handles the different package versions available. First of all, we need a package with some releases, let's develop it.

go package

The idea behind the package is to build something extremely simple, whose only purpose is to print its version on the terminal. I followed the best practice rules related to the semantic versioning: v<Major>.<Minor>.<Patch>. I've already deployed the project on my personal GitHub account github.com/enbis/versioningPack, but nothing stops you from making your own new one. Below the tree of the project folder.

~$ tree
.
├── go.mod
├── README.md
├── versioning.go
└── versioning_test.go
Enter fullscreen mode Exit fullscreen mode

the first release - v1.0.0

As a first release, it's fair to use the v1.0.0 as a version identification. So, let's write a few lines of code to achieve that.

The versioning.go file:

package versioning

import "fmt"

func GetVersion() {
    fmt.Println("Version 1.0.0 pulled from remote repository")
}
Enter fullscreen mode Exit fullscreen mode

The versioning_test.go:

package versioning

import "testing"

func TestVersioning(t *testing.T) {
    GetVersion()
}
Enter fullscreen mode Exit fullscreen mode

The go.mod file:

module github.com/enbis/versioningPack

go 1.13
Enter fullscreen mode Exit fullscreen mode

Well, everything seems to work as expected. The GetVersion function prints the version on the terminal and we can be satisfied with our job. It's time to push the changes and tag the first release with the v1.0.0 version.

Git versioning:

$ git add .
$ git commit -m 'first release is ready'
$ git push origin master
$ git tag v1.0.0
$ git push --tag origin master
Enter fullscreen mode Exit fullscreen mode

the second release - v1.1.0

Assumption: minors changes required. So, let's start working on the new features (in that specific case I'm just talking about change the string printed out). As soon as the string is modified (and tested the result), we are ready to push changes and tag in order to create a new release: v1.1.0.

The versioning.go file:

package versioning

import "fmt"

func GetVersion() {
    fmt.Println("Version 1.1.0 pulled from remote repository")
}
Enter fullscreen mode Exit fullscreen mode

Git versioning:

$ git add .
$ git commit -m 'second release is ready'
$ git push origin master
$ git tag v1.1.0
$ git push --tag origin master
Enter fullscreen mode Exit fullscreen mode

the first major release - v2.0.0

Assumption: new changes required. This time the changes modify the package so much so that the new version won't be longer backward compatible. It's time to create a new feature branch, called v2, to prevent incompatibility problems. That's not enough since we are developing a package so the reference on the go.mod file must also be aligned with the branch.

The versioning.go file:

package versioning

import "fmt"

func GetVersion() {
    fmt.Println("Version 2.0.0 pulled from remote repository")
}
Enter fullscreen mode Exit fullscreen mode

The go.mod file:

module github.com/enbis/versioningPack/v2

go 1.13
Enter fullscreen mode Exit fullscreen mode

Git versioning (and branching):

$ git checkout -b v2
$ git add .
$ git commit -m 'new release is ready'
$ git push origin v2
$ git tag v2.0.0
$ git push --tag origin master
Enter fullscreen mode Exit fullscreen mode

all the releases

The job with that package seems finished. Let's recap the versions available.

  • two releases on branch master: v1.0.0 and v1.1.0
  • one release on branc v2: v2.0.0
* 633ad5d (HEAD -> master, tag: v1.1.0, origin/master, origin/HEAD) version 1.1.0 ready
* 215b94a (tag: v1.0.0) rename
* 9e6c026 go mod master
| * 3aa6e2c (tag: v2.0.0, origin/v2, v2) rename
| * dc39bee go mod v2
| * 39f1fad v2.0.0
|/  
* 75ddd41 versioning
* 49d2c42 first commit
Enter fullscreen mode Exit fullscreen mode

the project

It's time to create a project in order to use the newly developed (and versioned) package. The purpose of that project is to test all the versions available of the github.com/enbis/versioningPack through the go.mod. As the last aspect, we will try to customize the behavior of the package locally and referring it instead of the release version.

Firstly, we need to create the development environment, out of the $GOPATH. Select the path you prefer and run the commands below.

~$ mkdir Projects/testVersioning
~$ cd Projects/testVersioning/
~/Projects/testVersioning$ touch main.go
~/Projects/testVersioning$ go mod init versPack
~/Projects/testVersioning$ ll
    totale 12
    drwxr-xr-x 2 enrico enrico 4096 apr 26 10:45 ./
    drwxr-xr-x 5 enrico enrico 4096 apr 26 10:42 ../
    -rw-r--r-- 1 enrico enrico   25 apr 26 10:45 go.mod
    -rw-r--r-- 1 enrico enrico    0 apr 26 10:43 main.go
~/Projects/testVersioning$ cat go.mod 
    module versPack

    go 1.13
Enter fullscreen mode Exit fullscreen mode

let's using the latest package's version available

Pretty easy use the latest version of the package: just pull it and the go.mod will search for the latest tag on the master branch.

~/Projects/testVersioning$ go get github.com/enbis/versioningPack
    go: finding github.com v1.1.0
    go: finding github.com/enbis v1.1.0
~/Projects/other/gomod$ cat go.mod 
    module versPack

    go 1.13

    require github.com/enbis/versioningPack v1.1.0 // indirect
Enter fullscreen mode Exit fullscreen mode

Let's try the package's feature in the main function.

package main

import (
    v1 "github.com/enbis/versioningPack"
)

func main() {
    v1.GetVersion()
}
Enter fullscreen mode Exit fullscreen mode

Looking at the output generated confirms the go.mod is referring to the v1.1.0.

~/Projects/testVersioning$ go run main.go 
    Version 1.1.0 pulled from remote repository
Enter fullscreen mode Exit fullscreen mode

let's using the package's version v1.0.0

Now, we'd like to work with the previous version of the versioningPack. We start deleting both go.mod and go.sum file, we need to init again the go.mod and pull a specific version of the github.com/enbis/versioningPack package. The secret lies in formatting the request when you pull the package, adding @ you can request a specific version among the tags contained on the master branch.

~/Projects/testVersioning$ rm go.mod go.sum
~/Projects/testVersioning$ go mod init versPack
~/Projects/testVersioning$ go get github.com/enbis/versioningPack@v1.0.0
    go: finding github.com v1.0.0
    go: finding github.com/enbis v1.0.0
~/Projects/other/gomod$ cat go.mod 
    module versPack

    go 1.13

    require github.com/enbis/versioningPack v1.0.0 // indirect
Enter fullscreen mode Exit fullscreen mode

As expected we pulled the v1.0.0. That's will be confirmed by running the main function.

~/Projects/testVersioning$ go run main.go 
    Version 1.0.0 pulled from remote repository
Enter fullscreen mode Exit fullscreen mode

let's using the package's version v2.0.0

What happened to our new major version? How can we pull it? If we try to get it using the request go get github.com/enbis/versioningPack@v2.0.0 an error occurs: invalid version module. That because v2.0.0 resides on the v2 branch. So, we need to specify it when we launch the request. Let's try with go get github.com/enbis/versioningPack/v2, the suffix matches the branch name.

~/Projects/testVersioning$ go get github.com/enbis/versioningPack/v2
~/Projects/other/gomod$ cat go.mod 
    module versPack

    go 1.13

    require github.com/enbis/versioningPack/v2 v2.0.0 // indirect
Enter fullscreen mode Exit fullscreen mode

All we need to do is rename the package, with the proper reference contained inside the go.mod.

package main

import v2 "github.com/enbis/versioningPack/v2"

func main() {
    v2.GetVersion()
}
Enter fullscreen mode Exit fullscreen mode

Trying it, the result is fairly obvious.

~/Projects/testVersioning$ go run main.go 
    Version 2.0.0 pulled from remote repository
Enter fullscreen mode Exit fullscreen mode

let's using two different version at the same time

Now, what we are going to do is using both major version in the same file. This is a little used case history in a normal case but is useful to explain the power of the module in Go. You can make the two major versions coexist in the same solution, as long as they reside on two different branches and have different references in their go.mod respectively.

~/Projects/testVersioning$ go get github.com/enbis/versioningPack/v2
~/Projects/other/gomod$ cat go.mod 
    module versPack

    go 1.13

    require (
        github.com/enbis/versioningPack v1.1.0 // indirect
        github.com/enbis/versioningPack/v2 v2.0.0 // indirect
    )
Enter fullscreen mode Exit fullscreen mode

As usual, two aliases are required to refer to the two versions of the same package.

package main

import (
    v1 "github.com/enbis/versioningPack"
    v2 "github.com/enbis/versioningPack/v2"
)

func main() {
    v1.GetVersion()
    v2.GetVersion()
}
Enter fullscreen mode Exit fullscreen mode

This is the output.

~/Projects/testVersioning$ go run main.go 
    Version 1.1.0 pulled from remote repository
    Version 2.0.0 pulled from remote repository
Enter fullscreen mode Exit fullscreen mode

let's testing a new feature of the package before versioning it

So far, we saw how to use different versions of the same package and how to make two different versions coexist together. Great, but there is more. You can replace the path of the package inside the go.mod file in order to refer to your own version of the same package. Just add the keyword replace and the path of the package you want to replace.

~/Projects/other/gomod$ cat go.mod 
    module versPack

    go 1.13

    require (
        github.com/enbis/versioningPack v1.1.0 // indirect
        github.com/enbis/versioningPack/v2 v2.0.0 // indirect
    )

    replace github.com/enbis/versioningPack => ../versioningPack
Enter fullscreen mode Exit fullscreen mode

That's is pretty clear, I copied the package one folder above the location of the project folder. The last line under the "require" defines the original versioningPack is now replaced with the local package. Let's try to make some changes to the local package.

package versioning

import "fmt"

func GetVersion() {
    fmt.Println("Version 1.1.1 from local folder")
}
Enter fullscreen mode Exit fullscreen mode

And run the main function of the project.

~/Projects/testVersioning$ go run main.go 
    Version 1.1.1 from local folder
    Version 2.0.0 pulled from remote repository
Enter fullscreen mode Exit fullscreen mode

conclusion

That's just the tip of the iceberg regarding Go packages and module but I hope you found this analysis interesting, despite the simplicity of the solution. Enjoy your time developing your new Go packages, which I'm sure will be much more useful than mine github.com/enbis/versioningPack.

💖 💪 🙅 🚩
enbis
enbis

Posted on April 26, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related