📜 ⬆️ ⬇️

Introduction to the Go Module System

The upcoming release of version 1.11 of the Go programming language will bring experimental support for the modules — a new dependency management system for Go. (comment perev .: release took place )


Recently I wrote about this small post . Since then, something has changed slightly, and we have become closer to release, so it seems to me that the time has come for a new article - add more practice.


So, here's what we’ll do: create a new package and then make a few releases to see how it works.


Module creation


First, create our package. Let's call it "testmod". An important detail: the package directory should be placed outside of your $GOPATH , because, inside it, module support is disabled by default . Go modules are the first step to total $GOPATH rejection in the future.


 $ mkdir testmod $ cd testmod 

Our package is quite simple:


 package testmod import "fmt" // Hi returns a friendly greeting func Hi(name string) string { return fmt.Sprintf("Hi, %s", name) } 

The package is ready, but it is not yet a module . Let's fix it.


 $ go mod init github.com/robteix/testmod go: creating new go.mod: module github.com/robteix/testmod 

We have a new file called go.mod in the package directory with the following contents:


 module github.com/robteix/testmod 

Not much, but this is exactly what turns our package into a module .


Now we can push this code into the repository:


 $ git init $ git add * $ git commit -am "First commit" $ git push -u origin master 

Until now, anyone willing to use our package would apply go get :


 $ go get github.com/robteix/testmod 

And this command would bring the latest code from the master branch. This option still works, but it would be better for us not to do this anymore, because now "there is a better way." It is dangerous to take code directly from the master branch, since we never know for sure that the authors of the package have not made changes that will “break” our code. To solve this particular problem, the Go modules were invented.


A small digression about module versioning


Go modules are versioned, plus there is some specificity of individual versions. You will have to become familiar with the concepts underlying semantic versioning .


In addition, Go uses repository tags when looking for versions, and some versions are different from the rest: for example, versions 2 and more should have a different import path than for versions 0 and 1 (we get to that).


By default, Go loads the latest version with a label available in the repository.
This is an important feature, since it can be used when working with the master branch.


What is important for us now is that when creating the release of our package, we need to put a label with the version in the repository.


Let's do it and do it.


Making your first release


Our package is ready and we can "release" it to the whole world. We do this with the help of version labels. Let the version number be 1.0.0:


 $ git tag v1.0.0 $ git push --tags 

These commands create a label in my Github repository that marks the current commit as release 1.0.0.


Go does not push this, but it’s a good idea to create an additional new branch ("v1") to which we can send fixes.


 $ git checkout -b v1 $ git push -u origin v1 

Now we can work in the master branch without worrying that we can break our release.


Using our module


Let's use the created module. We will write a simple program that imports our new package:


 package main import ( "fmt" "github.com/robteix/testmod" ) func main() { fmt.Println(testmod.Hi("roberto")) } 

Until now, you would run go get github.com/robteix/testmod to download the package, but with the modules it becomes more interesting. First we need to enable modules support in our new program.


 $ go mod init mod 

As you probably expected, based on what was read earlier, a new go.mod file with the module name inside appeared in the directory:


 module mod 

The situation becomes even more interesting when we try to build our program:


 $ go build go: finding github.com/robteix/testmod v1.0.0 go: downloading github.com/robteix/testmod v1.0.0 

As you can see, the go command automatically found and downloaded the package imported by our program.
If we check our go.mod file, we see that something has changed:


 module mod require github.com/robteix/testmod v1.0.0 

And we have another new file called go.sum , which contains package hashes to check the correctness of the version and files.


 github.com/robteix/testmod v1.0.0 h1:9EdH0EArQ/rkpss9Tj8gUnwx3w5p0jkzJrd5tRAhxnA= github.com/robteix/testmod v1.0.0/go.mod h1:UVhi5McON9ZLc5kl5iN2bTXlL6ylcxE9VInV71RrlO8= 

Making a release with bug fix.


Now, let's say we found a problem in our package: there is no punctuation in the greeting!
Some people will get mad, because our friendly greeting is no longer so friendly.
Let's fix this and release a new version:


 // Hi returns a friendly greeting func Hi(name string) string { - return fmt.Sprintf("Hi, %s", name) + return fmt.Sprintf("Hi, %s!", name) } 

We made this change right in the v1 branch, because it has nothing to do with what we will do next in the v2 branch, but in real life, you might need to make these changes to the master and then backport them to v1 . In any case, the fix should be in the v1 branch and we need to mark it as a new release.


 $ git commit -m "Emphasize our friendliness" testmod.go $ git tag v1.0.1 $ git push --tags origin v1 

Modules update


By default, Go does not update modules without demand. "And this is good," because we all would like to be predictable in our builds. If the Go modules would be updated automatically every time a new version comes out, we would return to the "Dark Ages to-Go1.11". But no, we need to tell Go to update the modules for us.


And we will do it with the help of our old friend - go get :



In this list there is no way to upgrade to the latest major version. There is a good reason for this, as we shall soon see.


Since our program used version 1.0.0 of our package and we just created version 1.0.1, any of the following commands will update us to 1.0.1:


 $ go get -u $ go get -u=patch $ go get github.com/robteix/testmod@v1.0.1 

After starting (say go get -u ), our go.mod has changed:


 module mod require github.com/robteix/testmod v1.0.1 

Major versions


In accordance with the specification of semantic versioning, the major version differs from the minor version. Major versions may break backward compatibility. From the point of view of the Go modules, the major version is a completely different package .


It may sound wild at first, but it makes sense: two versions of the library that are incompatible with each other are two different libraries.


Let's make a major change in our package. Suppose, over time, it became clear to us that our API is too simple, too limited for our users usceices ’, so we need to change the Hi() function to accept the greeting language as a parameter:


 package testmod import ( "errors" "fmt" ) // Hi returns a friendly greeting in language lang func Hi(name, lang string) (string, error) { switch lang { case "en": return fmt.Sprintf("Hi, %s!", name), nil case "pt": return fmt.Sprintf("Oi, %s!", name), nil case "es": return fmt.Sprintf("¡Hola, %s!", name), nil case "fr": return fmt.Sprintf("Bonjour, %s!", name), nil default: return "", errors.New("unknown language") } } 

Existing programs that use our API will break down because they a) do not pass the language as a parameter and b) do not expect an error to return. Our new API is no longer compatible with version 1.x, so meet version 2.0.0.


Earlier, I mentioned that some versions have features, and now such a case.
Versions 2 and more should change the import path. Now they are different libraries.


We will do this by adding a new version path to the name of our module.


 module github.com/robteix/testmod/v2 

Everything else is the same: push, put a label that is v2.0.0 (and optionally sod v2)


 $ git commit testmod.go -m "Change Hi to allow multilang" $ git checkout -b v2 # optional but recommended $ echo "module github.com/robteix/testmod/v2" > go.mod $ git commit go.mod -m "Bump version to v2" $ git tag v2.0.0 $ git push --tags origin v2 # or master if we don't have a branch 

Major version upgrade


Even though we have released a new, incompatible version of our library, the existing programs are not broken , because they continue to use version 1.0.1.
go get -u will not download version 2.0.0.


But at some point, as a library user, I may want to upgrade to version 2.0.0, because, for example, I am one of those users who need support for multiple languages.


To upgrade, you need to change my program accordingly:


 package main import ( "fmt" "github.com/robteix/testmod/v2" ) func main() { g, err := testmod.Hi("Roberto", "pt") if err != nil { panic(err) } fmt.Println(g) } 

Now, when I start go build , it “comes off” and will download version 2.0.0 for me. Note that although the import path now ends with "v2", Go still refers to the module by its real name ("testmod").


As I said, the major version is a different package in all respects. These two Go modules are unrelated. This means that we can have two incompatible versions in one binary:


 package main import ( "fmt" "github.com/robteix/testmod" testmodML "github.com/robteix/testmod/v2" ) func main() { fmt.Println(testmod.Hi("Roberto")) g, err := testmodML.Hi("Roberto", "pt") if err != nil { panic(err) } fmt.Println(g) } 

And it eliminates the common problem with dependency management, when dependencies depend on different versions of the same library.



Let's go back to the previous version, which uses only testmod 2.0.0 - if we now check the contents of go.mod , we notice something:


 module mod require github.com/robteix/testmod v1.0.1 require github.com/robteix/testmod/v2 v2.0.0 

By default, Go does not remove dependencies from go.mod until you ask for it. If you have dependencies that are no longer needed, and you want to clean them, you can use the new tidy command:


 $ go mod tidy 

Now we have only those dependencies that we really use.


Vending


Go modules ignore the vendor/ directory by default. The idea is to gradually get rid of vendoring 1 . But if we still want to add "checked out" dependencies to our version control, we can do it:


 $ go mod vendor 

The team will create the vendor/ directory in the root of our project, containing the source code of all dependencies.


However, the go build by default still ignores the contents of this directory. If you want to collect dependencies from the vendor/ directory, you must explicitly ask for this.


 $ go build -mod vendor 

I assume that many developers who want to use vendor will run go build , as usual, on their machines and use -mod vendor on their CIs.


Again, the Go modules are moving away from the idea of ​​vendoring to using proxies for modules for those who do not want to directly depend on the upstream version control services.


There are ways to ensure that go will not be available network (for example, using GOPROXY=off ), but this is the topic of the next article.


Conclusion


The article may seem complicated to someone, but this is due to the fact that I tried to explain a lot of things at once. The reality is that Go modules are generally simple today - we, as usual, import the package into our code, and the go command does the rest for us. Build dependencies are automatically loaded.


The modules also eliminate the need for $GOPATH , which was a stumbling block for new Go developers who had trouble understanding why you need to put something in a particular directory.


Vending (unofficially) declared obsolete in favor of using a proxy. one
I can make a separate article about proxies for Go modules.


Notes:


1 I think this is too loud, and some may have the impression that the vendoring is being removed right now. This is not true. Vending still works, albeit slightly differently than before. Apparently, there is a desire to replace the vendor with something better, for example, a proxy (not a fact). For now, it's just a desire for a better solution. Vending will not go away until a good replacement is found (if there is one).


')

Source: https://habr.com/ru/post/421411/


All Articles