Domain-Free Dependencies in Go Modules

Domain-Free Dependencies in Go Modules

Dependencies play a critical role in every programming language. All programming languages use libraries as a methodology for packaging and reusing code; dependencies are references to those libraries that a software package needs to run. Because software developers are engineers, they like to make use of code that’s tested and ready to go.

The Release Equivalence Principle, one of the Principles of Package Cohesion, teaches us that code should not simply be copied and pasted from place to place as a means of reusing it. That would be incredibly inefficient and require continuous maintenance of copies of the same code. Instead, this principle guides us to package related code together into a reusable unit which can then be included in other applications without code duplication, or the need to reinvent the wheel.

Additionally, because packages are released through a source control mechanism, releases can be identified by a specific version number; this is known as “versioning.” Software developers are free to include the specific version they need in their application, and only update to a more recent one when they are ready to support it. This way, they can ensure that their dependency management process is stable.

Dependencies in Golang

Dependencies in Go have quite a long history which you can find described in more detail in our post about vgo from a couple of years ago. At that time, Go dependencies came with a number of inconveniences. Among them was the reliance on a hard-coded domain (github.com, bitbucket.org, etc.) If a developer changed the service provider for their versioning system, Go considered it a completely new library!

On the flip-side, domain-free dependency management is the norm in Java. The package name and the source of the JAR file are two different things. In fact, Java makes it even more straightforward because the dependencies are compiled and packed directly into the JAR file. Then the JAR file can be placed in any repository located on any domain.

Go Modules

Go modules use Git as the mechanism for including their dependencies. When Go tries to retrieve the dependency, it clones the repository under the hood. This effectively relates the Go dependencies directly to the Git repository (and its domain) from which it was imported.

Go modules were introduced about two years ago, and Spiral Scout hoped it would solve the frustrating domain dependency problem. Unfortunately, it did not. The packages in Go modules are expected to contain the domain name where they are pulled from. However, Go modules can contain a “replace” stanza in the go.mod file.

How We Achieved Domain Freedom

One of Spiral Scout’s larger clients decided to move from one Git hosting provider to another, and we decided to try to find a way to achieve domain-free dependencies in their code. To do this, we knew we needed to reference the domain name only inside the go.mod file and that all of our custom modules needed to avoid using the domain name of the module repo.

Using the Summ Package

To start, we created this library. It is a simple module that includes an internal “summ” package with a function for summing two numbers together. The project’s root package contained a facade sum function that imported and used the sum function from the “summ” package. The module was named “godeps_lib,” and as you can see in the code snippet below, the facade function did not contain the domain name when it imported the internal “summ” package:

go
package godeps_lib

import (
	"godeps_lib/summ"
)

func Summ(a, b int) int {
	return summ.Summ(a, b)
}		

This option used a library with internal dependencies that did not directly reference the domain name.

Then, we created the “service” imitation that could import and use the “library”. As you see, the service executable also used the library without needing to reference the domain:

go
package main

import (
	"fmt"
	"godeps_lib"
)

func main() {

	fmt.Println(godeps_lib.Summ(3, 3))

}		

In the code example below, we used the go.mod file with the “replace” directive, which was the only place we could see the real disposition of the “library” godeps_lib:

go
module svc

go 1.14

require godeps_lib v0.0.0

replace godeps_lib v0.0.0 => github.com/guntenbein/godeps_lib v0.0.0-20200609133944-0dafe9608ecb		

Let’s examine quickly what happened when we moved the library to another domain/cloud Git service. We discovered, for example, that we could copy the library to bitbucket.org without any changes.

Then we changed the go.mod file for the “service.” Changes were made solely to the go.mod and go.sum files. You can see exactly what changed in this diff.

Ultimately, everything ran and compiled without domains inside the code.

Limitation

One problem that we found in our testing was that when the “$go get” command ran, which downloaded and installed dependencies with the correct versions, it failed with an error because of an incorrect path for the module:

Go Limitations

Conclusion

We can use “go mod” and have dependencies on real domains but only in the go.mod specification file. This is achieved with the “replace” directive of go modules https://golang.org/ref/mod#tmp_15 that was initially intended for local code usage. This may cause some side effects such as the “$go get” command result described above. Therefore, we would recommend experimenting with this a bit more before using it in production.