2024-05-21
Go's old $GOPATH
story for development and dependencies
As people generally tell the story today, Go was originally developed without support for dependency management. Various community efforts evolved over time and then were swept away in 2019 by Go Modules, which finally added core support for dependency management. I happen to feel that this story is a little bit incomplete and sells the original Go developers short, because I think they did originally have a story for how Go development and dependency management was supposed to work. To me, one of the fascinating bits in Go's evolution to modules is how that original story didn't work out. Today I'm going to outline how I see that original story.
In Go 1.0, the idea was that you would have one or more of what are
today called multi-module workspaces. Each workspace contained
one (or several) of your projects and all of its dependencies, in
the form of cloned and checked-out repositories. With separate
repositories, each workspace could have different (and independent)
versions of the same packages if you needed that, and updating the
version of one dependency in one workspace wouldn't update any other
workspace. Your current workspace would be chosen by setting and
changing $GOPATH
, and the workspace would contain not just the
source code but also precompiled build artifacts, built binaries,
and so on, all hermetically confined under its $GOPATH
.
This story of multiple $GOPATH
workspaces allows each separate
package or package set of yours to be wrapped up in a directory
hierarchy that effectively has all of its dependencies 'vendored'
into it. If you want to preserve this for posterity or give someone
else a copy of it, you can archive or send the whole directory tree,
or at least the src/ portion of it. The whole thing is fairly similar
to a materialized Python virtual environment.
(The original version of Go did not default $GOPATH
to $HOME/go
,
per for example the Go 1.1 release notes. It would take until Go 1.8 for
this default to be added.)
This story broadly assumes that updates to dependencies will normally be compatible, because otherwise you really want to track the working dependency versions even in a workspace. While you can try to update a dependency and then roll it back (since you normally have its checked out repository with full history), Go won't help you by remembering the identity of the old, working version. It's up to you to dig this out with tools like the git reflog or your own memory that you were at version 'x.y.z' of the package before you updated it. And 'go get -u' to update all your dependencies at once only makes sense if their new versions will normally all work.
This story also leaves copying workspaces to give them to someone else (or to preserve them in their current state) as a problem for you, not Go. However, Go did add 'experimental' support for vendoring dependencies in Go 1.5, which allowed people to create self-contained objects that could be used with 'go get' or other simple repository copying and cloning. A package that had its dependencies fully vendored was effectively a miniature workspace, but this approach had some drawbacks of its own.
I feel this original story, while limited, is broadly not unreasonable. It could have worked, at least in theory, in a world where preserving API compatibility (in a broad sense) is much more common than it clearly is (or isn't) in this one.