I have been pretty vocal about my feelings regarding the Go programming language — I absolutely love the language but I also abhor the way it deals with dependency management.
The fact that we’ve gone from glide to dep to vgo all without ever having a well and truly official stance on dependency management has aggravated the situation. At the core of my objection to Go’s dependency management model is that inside my .go files, directly in the code, I must not only declare on what I depend, but from where I should get it. I’ve never liked seeing github paths in my code and I never will. This feels like personal preference, as I know a lot of people who actually like the github paths.
My frustration stems not just from the high degree of friction I tend to find in resolving, downloading, and satisfying dependencies in an automated, docker-based workflow…but also from how some tasks that should be “trivially easy” are actually difficult and annoying in Go. For example, if I create a fork of a project that has a main.go file that imports another package in that project…the path to that project will immediately be wrong. If I’m working in the fork, I have to change the path to my path. When I submit a pull request back to the originating branch, I have to manually change the path back before committing. It blows my mind that a workflow like this has not been smoothed out to the point of being trivial.
For those of you reaching for your red ink comment markers — yes I know it’s a single line edit, and you can do it with sed or whatever. That’s not the point. The point is that little side-effects of conflating the location, means of satisfying, and declaration of dependencies has consequences. Some are big, some are small. All of them could’ve been avoided.
Ok, I am now off my soapbox. All of my complaints in this area relate to Go 1.10 and earlier. The interwebs say that Go 1.11’s module system might actually solve some of my problems. So I tried it out. I created a new project in a non-GOPATH directory with one external dependency and typed:
$ go mod init github.mycompany.com/newproject/shiny
This defined the root path of my module. Every import that shares this path prefix is going to be assumed to be in the same directory as the
go.mod file. In Go 1.10, if I had created an executable application in the
cmd/cli sub-directory and attempted to import a package like
platform/tcp from the project root, I would’ve had to make sure that my
$GOPATH reflected this hierarchy. More importantly, I couldn’t specify a relative path so, for example, if I was working in a fork I wouldn’t find this package from my
With Go 1.11, I can do
import github.mycompany.com/newproject/shiny and use the
replace keyword in the
go.mod file to swap that location to a relative path or make other alterations. This makes me immune from the “fork problem”. It also means this project can be downloaded on any machine, regardless of the developer’s
$GOPATH, and it should be able to locate dependencies and compile.
Once you’ve got a module defined for your Go project, you’ll have a
go.mod file. Thankfully, after some digging around and suffering through the fog of early Monday morning, Twitter and the Internet revealed to me some ways I can avoid manually editing this file.
Any time you use
go get or
go build or
go test, it’ll modify your
go.mod file. If your module file is listing a requirement pointing at the master branch, the next time that dependency is required, your
go.mod file will swap out the word
master for the specific commit in github. This means that, like the
.lock files from other dependency managers, you are guaranteed that subsequent builds will use the exact same versions of dependencies. The module file also lists out transitive dependencies so that your entire chain of dependencies can be pinned to specific versions.
My first attempt using Go modules was pretty unremarkable — I built a “hello world” app and it worked. Then I grabbed one of my previous projects with a fairly deep directory tree based on Bill Kennedy’s package oriented design. I was able to get this project to build outside my
$GOPATH, and I was able to get all of my internal references to work without having to edit the
go.mod file to indicate relative paths.
This might not seem like such a big deal to some Go developers, especially those who have become apologists for the current state of things because of habits they’ve developed, but Go 1.11 is huge for my workflow. All of these new changes can be simmered down to the following for my needs:
- I can build Go applications anywhere on my hard drive, and not just in that [CENSORED] github path.
- I no longer hate forking with Go projects
go getwill maintain most of my necessary updates to dependencies
go mod vendorwill spit out a vendor directory required for my automated build process based on my
go mod tidydetects new dependencies from the source code and adds them to the
go.modfile, using a pretty good version pinning algorithm
- I can edit the
go.modfile manually with
replaceto accommodate some specific enterprise requirements for internal source repositories
So, in conclusion, Go’s dependency management still personally chafes visually when reading my import sections. All that said, I will never use 1.10 again and you can pry Go modules from my cold, dead hands. The “death of a thousand cuts” dependency management rage has been almost entirely eliminated with Go modules.
Modules are for doing more than just adding version numbers to import paths, and I’m planning on migrating all of my code to use them. Obviously your mileage may vary, but I’m actually looking forward to coding in Go again just because of modules. If you haven’t already, you should try them out for yourself and see if it is as impactful for you as it was for me.