Exploring vgo

Yesterday Russ Cox announced vgo, a drop in replacement for the go tool designed to handle package versioning. While it is still an experiment, it is a pretty unexpected change given that everyone thought dep was going to become the official dependency management tool. If you haven’t already, you should start by reading Russ’ post as well as the tour he provides. It is a great overview of what vgo is, even if many people have misinterpreted a few things in the post.

When I first read the article, I had feelings similar to what I suspect others felt. I was confused about a few points, concerned about a few potential issues, and overall unsure of how I felt. I wanted to get a better understanding of what vgo really was, so I opted to both read the vgo tour, and to download vgo and experiment a bit. You can actually check out some of the repos I created during my experimentation on GitHub: https://github.com/joncalhoun?tab=repositories

After getting a better understanding of vgo, I was surprised that a lot of the assumptions people were making were incorrect despite being clearly covered in the tour and easily double checked by using vgo. Unfortunately I doubt everyone will go download the tool before complaining, so this post is intended to help clarify on a few of the misunderstandings I saw, while also giving me a chance to express my excitement for the project 😁

How are dependency versions resolved?

Oddly enough, this was a major source of confusion yet it was made pretty clear in the tour. I suspect many people read the original post, misunderstood what Russ was saying, and then never bothered to read the tour where things would have been cleared up.

There are only three circumstances that need to be considered, and each are described below.

New dependencies

The first use case is incredibly simple. When a new package is added to a project as a dependency, vgo will opt for the NEWEST version of that package unless you manually specify otherwise. I don’t see any reason why anyone would something different, and this point doesn’t seem to concern anyone except the people who incorrectly believe vgo will opt for the oldest version, which is proven incorrect in the vgo tour.

Existing dependencies with major version mismatch

Most package managers wouldn’t permit this. vgo allows this by expecting major versions to have a different import path - one with /v2 (or any other major version number) appended to it - which would make it a completely different package than other major versions.

It is slightly unclear to me at this time how developers are supposed to manage this, but I suspect Russ will clarify a bit over the coming week. Regardless, this is pretty much the only way to handle this (aside from banning multiple major versions), so it isn’t a huge point of contention and I would prefer to wait for more information before discussing it further.

Existing deps with the same major version

Assuming major versions are the same, dependencies are resolved by choosing the highest version in the list of minimum versions. That is, if 3 modules all require the turtle package we might have a set of MINIMUM versions like:

vgo would then determine that v1.3.2 is the highest semver version in this set of minimum versions, so that is the version that would be used.

I suspect this has confused many people because Russ speaks about selecting the minimum version, but what he meant by this was that the build process WOULD NOT use v1.3.3 even if it was available, and would instead opt for the minimum version that satisfies all of its known requirements.

Ways to upgrade dependencies

Now that we understand how versions are determined, let’s take a look at the few ways you can initiate a dependency upgrade.

Earlier in this post we had the turtle example where we ended up with the following minimum version requirements from all our modules:

We are going to continue to build on this imaginary project, looking at ways that turtle could be upgraded.

Manually upgrading

This is pretty self explanatory; if you want to start using v1.4.4 of turtle, you can simply update your module to specify v1.4.4 as the minimum version. vgo provides tools to choose a specific version, upgrade to the latest version, and complete a variety of other tasks to help with this. Check out the vgo tour for info on how to do this.

Adding or update a dependency

The only other way a dependency can be upgraded is if you add or update an existing dependency. This happens because transitive dependencies also update, so when you upgrade package foo (or add it), it might also cause package turtle to update if the minimum version requirements change.

It is also important to remember that these transitive dependencies can be arbitrarily deep. That is, package foo could require package bar, which requires package spaz, and so on until finally a package requires turtle@v1.3.6, which would cause our version of turtle to upgrade from v1.3.2 to v1.3.6 because this is the new minimum version that satisfies all of our modules.

Potential issues (and counter arguments)

Now that we have a proper understanding of how vgo decides which version to use, and how to upgrade packages, let’s take a look at some of the most common concerns people had with vgo.

Transitive dependencies won’t get updated

I saw this in a few places, but it simply isn’t true. vgo takes the dependency requirements of all your modules into consideration when determining which version of a package to use. This also means that if you upgrade package foo, you are guaranteed to have packages that meet or exceed the minimum version requirements specified by foo’s go.mod file.

Security updates won’t be opt-in by default

The basic claim here is that if we are using v1.2.0 of a package, we should be able to specify whether we want to automatically use the most recent patch (v1.2.X) for security updates, or we could opt into any minor version updates automatically (v1.X.Y).

With vgo, this update won’t automatically occur. Instead, you would need to manually tell vgo to update your packages using something like vgo get -u.

While this argument does have some merit, I’m not sure how much weight I’d give it. For starters, we aren’t going to ever update our code without creating a new build, so any security patches released while our code is deployed won’t be added until we do a new release. If that isn’t the case, I would be terrified of having different version of my dependencies in different servers at the same time, as this could be a nightmare to debug and manage.

That leaves us with this issue only really being an issue when we build, but that isn’t really an issue. With the change to what vgo test all means, you could fairly easily automate your build process to automatically update all of your dependencies, test, and then deploy if tests pass much like you would expect to see with another dependency manager. It would be concerning if vgo prevented this, but given that it doesn’t I’m not sure why this would be a major concern. You can customize your build pipeline, but that doesn’t mean your customizations should definitely be in the standard library’s tooling.

Russ Cox also addresses this point in a reply on the golang-nuts mailing list:

In the tour (research.swtch.com/vgo-tour) I show how “all” is redefined to be useful again. So it’s completely reasonable to try an update, go test all, and if all the tests pass (and you trust your tests), then check in the go.mod file. Someone could build a bot to do this automatically, even. I don’t think minimal version selection means you’re always stuck with old versions. It just means you don’t get new versions until you ask for them and are ready to evaluate how good they are, not just because you run ‘go get’ and a new version has appeared overnight.

Packages will be harder to maintain

The general argument here is that as a package manager, it will be harder to support your users when they aren’t automatically being upgraded to the latest version.

As a counterpoint, I’d like to point out that the reverse is also true. If I run package turtle and it depends on package foo, it isn’t uncommon to have to deal with issues when a new version of foo is released, has a breaking change, and dependency managers all update to the latest version of foo by default. When this happens, packages like turtle will almost always get an issue submitted when in reality turtle isn’t to blame.

This makes me wonder which would actually cause more issues for package maintainers - users automatically updating all their dependencies by default, or users generally leaning towards the minimum required versions you have certified to work with your package in your go.mod file.

In both scenarios you will have people using outdated versions of the package as well as users who are constantly updating all their dependencies, but at this point I’m not sure we really understand whether leaning towards one or the other causes more issues.

I also feel this isn’t an incredibly hard problem to solve at the issue level. When someone wants to submit an issue, ask them to upgrade versions first. This isn’t uncommon, especially with desktop applications like Atom, and is a completely reasonable request for a package maintainer to make.

Accidental breakage

Imagine we are working on the foo package and accidentally added a backwards incompatible change to v1.3.2, but then fix it in v1.3.3. What would happen if vgo determined that v1.3.2 meets our minimum requirements, but that incompatible change causes our code to break? Wouldn’t it be better to automatically use v1.3.3?

This is a very specific and rare occurrence, and I don’t see it happening frequently in practice. For it to occur, a package you depend on needs to get updated (or added) manually, and that update needs to include a transitive dependency that sets the minimum version of foo to v1.3.2 (the broken version). Most of the time these broken builds are fixed fairly quickly, so having another package set it as its minimum version seems extremely unlikely. Even if it does occur, I suspect the package with the bad minimum version would get updated promptly.

If this does happen, the solution is fairly simple:

  1. Inform the package that you updated about the v1.3.2 breakage so they can release an updated go.mod.
  2. Update your go.mod to require a minimum version of foo@v1.3.3.

What is especially nice about vgo in this scenario is that by its very nature, updates only occur when a developer starts them, so a developer will not only be there ready to debug the issue, but will be prepared to handle issues like this as they are initiating an upgrade. This is a logical time for issues like this to occur. Build time, on the other hand, is not when you expect issues like this to occur or be handled.

Offline builds

I saw a few users concerned about offline builds, and I had the question myself, but was informed by tv64738 on reddit that vgo currently uses the $GOPATH/src/v/ directory to store the source code allowing for offline builds once you resolve dependencies once. It is unclear if this is a permanent solution, but it does imply that this is on Russ’ mind which is a good thing.

HTTP is insecure

A few users were concerned that repositories were being downloaded via regular old HTTP instead of HTTPS. As far as I can tell, the source for vgo seems to be using HTTPS, and Russ’ use of “HTTP” in the blog post likely just meant the HTTP protocol instead of say git or bzr.

API limits

A few users noticed that github has some API limits that kick in and require some setup to make vgo continue working w/out getting errors, but I suspect issues like this could be overcome with some collaboration between GitHub and Google.

Benefits of this approach

While this post is mostly meant to clarify and discuss a few concerns people have with vgo, I think it is also worth pointing out some of the perks to this approach. This is nowhere near an exhaustive list, but are some points I personally resonated with.

It “just works” for the most part

I definitely love that we started to get the community converging on a single tool (dep), but it is still a tool that developers need to learn and become comfortable with.

vgo on the other hand feels fairly natural so far. The hardest part might be getting people comfortable with the go.mod files, but if that ends up being the hardest part I’d call that a success.

go test all is way more useful

vgo also makes go test all way more useful, as it only tests your current module and all of its dependencies. While this didn’t seem to be a primary goal of vgo, it was intentional and I am loving it.

More generally speaking, I am hoping that the move way from a $GOPATH setup allows tools like gorename, which (iirc) won’t work if you have any broken code in your $GOPATH/src directory, to start working more consistently because they can be leveraged in individual module contexts, making it much more likely that all the code there is building correctly.

Builds should be more consistent

I doubt we can ever make builds 100% consistent, but the approach vgo is taking should avoid any of those painful issues that occur when you go to build and a new version of foo was released last night. That doesn’t mean you can’t get the latest version of foo, but you won’t be forced to use it unless you explicitly state you want it.

In the words of Russ:

I don’t think minimal version selection means you’re always stuck with old versions. It just means you don’t get new versions until you ask for them and are ready to evaluate how good they are, not just because you run ‘go get’ and a new version has appeared overnight.

With a build process that can auto-upgrade a package version, any random developer could end up needing to fix the issue. Or worse yet, EVERY developer on your team could be blocked because of an issue caused by a new version of a package, and when that happens chances are 2+ devs are going to go try to fix the same problem at the same time wasting resources. With vgo, this issue only occurs when someone sets out to upgrade a package, so the likelihood of it blocking an entire team or being tackled by multiple developers at the same time are decreased significantly.

While this often isn’t considered when discussing build consistency, I can say from experience that a breaking change like this is a huge time waster for teams and is exactly what leads to teams specifying EXACT versions of packages that they use with no wiggle room at all.

Maximum local control, minimal external control

Another important point that Russ makes in the golang-nuts mailing list is that this approach allows you to completely control your own builds, but limits your control over people who use your code.

In his own words:

Yes, I’ll make that point again in tomorrow’s posts elaborating minimal version selection, but I think this is probably the most important algorithmic policy detail. You get total control over your own build. You only get limited control over other people’s builds that happen to import your code.

I suspect we will see Russ elaborate on this one a bit, but the main takeaway for me is that this limits a package’s ability to say “My code only works with foo@v1.2.1!” and instead the package can only say “My code works with foo@v1.2.1 or greater.“, which is much more likely to resolve nicely with other packages you use.

This pain is summed up pretty nicely by David Anderson in the mailing list:

I’ve struggled with glide and dep dependency hell a lot in my Go projects (especially those that use the Kubernetes client library, which abuses “I, the library author, can define byzantine constraints on my users” to the extreme). The way I’ve described it informally is that traditional “arbitrary version selection” algorithms create the wrong incentive structure for library authors, in the sense that they have all the power to lock versions arbitrarily, but bear none of the pain associated with their decisions - instead, the binary author at the apex of the dependency tree gets that pain. “Authority without responsibility” is the death of many incentive structures.

Learn Web Development with Go!

Sign up for my mailing list and I'll send you a FREE sample from my course - Web Development with Go. The sample includes 19 screencasts and the first few chapters from the book.

You will also receive emails from me about Go coding techniques, upcoming courses (including FREE ones), and course discounts.

Avatar of Jon Calhoun
Written by
Jon Calhoun

Jon Calhoun is a full stack web developer who teaches about Go, web development, algorithms, and anything programming. If you haven't already, you should totally check out his Go courses.

Previously, Jon worked at several statups including co-founding EasyPost, a shipping API used by several fortune 500 companies. Prior to that Jon worked at Google, competed at world finals in programming competitions, and has been programming since he was a child.

Related articles

Spread the word

Did you find this page helpful? Let others know about it!

Sharing helps me continue to create both free and premium Go resources.

Want to discuss the article?

See something that is wrong, think this article could be improved, or just want to say thanks? I'd love to hear what you have to say!

You can reach me via email or via twitter.

Recent Articles All Articles Mini-Series Progress Updates Tags About Me Go Courses

©2024 Jonathan Calhoun. All rights reserved.