Go package: a brief tour into the world of Go Module
enbis
Posted on April 26, 2020
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
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")
}
The versioning_test.go:
package versioning
import "testing"
func TestVersioning(t *testing.T) {
GetVersion()
}
The go.mod file:
module github.com/enbis/versioningPack
go 1.13
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
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")
}
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
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")
}
The go.mod file:
module github.com/enbis/versioningPack/v2
go 1.13
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
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
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
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
Let's try the package's feature in the main function.
package main
import (
v1 "github.com/enbis/versioningPack"
)
func main() {
v1.GetVersion()
}
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
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
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
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
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()
}
Trying it, the result is fairly obvious.
~/Projects/testVersioning$ go run main.go
Version 2.0.0 pulled from remote repository
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
)
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()
}
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
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
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")
}
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
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.
Posted on April 26, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.