Skip to content
gohacks.dev
Basics

How GOPATH Gave Way to Go Modules and What Remains

GOPATH once decided where every Go project lived. Modules replaced that in Go 1.11, and what is left of it today is a cache and a bin directory.

6 min read
Summarize in AI
Flat illustration of a dependency tree of packages with version tags.

If you learned Go from a tutorial written before 2019, it told you to set GOPATH, make a src directory under it, and clone every project into a path that mirrored its import URL. Following that today does nothing useful, and the confusion it causes is one of the commonest reasons people think Go setup is harder than it is. The short version is that modules replaced the whole idea, and a little of GOPATH survived as plumbing. This is what the old mode did, why it was replaced, and exactly which parts you still touch. It belongs with the rest of the Go basics, because every project you start sits on it.

Tested with Go 1.25.6, macOS on Apple Silicon (arm64), with GO111MODULE=off used to reproduce the old mode

How the old mode worked

Go's first design assumed one workspace. You set GOPATH to a directory, and inside it three folders had fixed meanings: src for source code, pkg for compiled package archives and bin for installed commands. The import path of a package was its directory path under src, so a package at $GOPATH/src/example.test/legacy/greet was imported as example.test/legacy/greet. There was no version in that path, and no file recorded which revision of a dependency you had built against.

I rebuilt that layout in a temporary directory to show it running. The program imports a package from its own tree:

Shell
export GOPATH=/tmp/gp
cd $GOPATH/src/example.test/legacy
GO111MODULE=off go run .
Text
hello from GOPATH

No go.mod, no download step, and it works because the directory sits at the path the import statement names. GO111MODULE=off is the switch that turns this mode on in a modern toolchain. Run the same command with modules on and it fails:

Text
go: go.mod file not found in current directory or any parent directory; see 'go help modules'

That second message is the one beginners meet. It is not telling you to configure GOPATH. It is telling you the directory is not a module, and go mod init fixes it.

What went wrong with GOPATH

Three problems pushed the Go team to replace it, and each one still matters when you read old code.

There were no versions. go get fetched the latest commit of a dependency and put it in src. Two projects on the same machine shared one copy, so upgrading it for one project could break the other. Teams worked around this with vendoring tools such as dep, glide and govendor, each with its own file format.

Location was part of identity. A project had to live at a path matching its import path, so you could not keep a clone in ~/code/work/service and build it. Forks were painful: your fork of a library lived at a different path, so you edited every import or faked the directory layout.

Builds were not reproducible. Two people running go get a week apart could build different code from the same repository. Nothing recorded the difference.

Modules, introduced as an experiment in Go 1.11 and described in the Go Modules Reference, fix all three at once. A module is a tree of packages with a go.mod file at the root. The file names the module path, the minimum Go version and every dependency with an exact version. A second file, go.sum, holds cryptographic checksums of those dependencies, so a changed download is detected rather than trusted.

The timeline that matters

ReleaseWhat changed
Go 1.11Modules arrive, opt in with GO111MODULE=on outside GOPATH
Go 1.13Module mode becomes the default whenever a go.mod exists, and the module proxy and checksum database are turned on
Go 1.16Module mode is on by default everywhere, and go install pkg@version is introduced
Go 1.17go get for installing binaries is deprecated
Go 1.18go get no longer builds or installs; it only edits dependencies

If a tutorial says to use go get github.com/foo/bar to install a command, it predates 1.18. The modern spelling is go install github.com/foo/bar@latest. The first changes your go.mod, and the second puts a binary in your bin directory.

What GOPATH still does

Run go env GOPATH GOMODCACHE on a fresh install and you see where the plumbing lives. On my Mac the first was a directory named go in my home folder and the second sat inside it as pkg/mod. Two directories under it are still in use:

PathWhat it holds today
$GOPATH/pkg/modThe module cache. Every dependency version you ever downloaded sits here, read only, shared by every project on the machine
$GOPATH/binBinaries from go install, unless you set GOBIN

The src directory is gone from the workflow, and the module cache is why disk usage grows quietly as you try libraries. go clean -modcache empties it, and everything it held is fetched again on demand. Because the cache is shared and read only, two projects at different versions of a library each get their own directory, which is the property GOPATH never had.

$GOPATH/bin is the one people forget to put on their PATH. That is why go install seems to do nothing: the binary exists but the shell cannot see it. The fix is on the install page, covered in the install guide.

A module can live anywhere

The practical gain is the freedom to put code wherever you like. I created a module under /tmp for this article, wrote a main.go, and ran it with no GOPATH involvement at all. go mod init created the file, and go list -m all showed the module with its dependency list, which is empty for a program using only the standard library:

Shell
cd /tmp/hh
go mod init example.test/hello
go list -m all
Text
example.test/hello

Add a dependency with go get and the go.mod grows a require line and go.sum appears. Replace directives and workspaces come later. For a first project you need only go mod init and go mod tidy, and the Go team's Using Go Modules walks through the first dependency step by step.

Mixed signals you will see in the wild

A few leftovers still turn up, and it helps to recognize them.

A vendor directory. Vendoring survives as go mod vendor, which copies dependencies into the repository so a build needs no network. It is optional and a build uses it only when -mod=vendor is on or the vendor folder is consistent with go.mod. Some teams keep it for hermetic builds, and most do not.

GOPATH in CI scripts. Many pipelines still export GOPATH=... and mkdir -p $GOPATH/src/... out of habit. It is harmless but pointless. Setting GOCACHE and GOMODCACHE to a cached directory is what actually speeds up a pipeline.

The undefined: someName errors after moving a project. If you copy a project from GOPATH days and its imports point at a path that no longer exists, the compiler reports undefined packages. Undefined name errors in Go shows how to tell a missing import from a missing module.

Where GOPATH mode is still the right answer

Almost nowhere. If you maintain a very old codebase that has never had a go.mod, you can still build it with GO111MODULE=off, and it is a fine place to start before running go mod init on it, since the migration is mostly a single command plus a clean-up. Some Linux distributions build Go packages in a GOPATH layout for historical reasons. If you contribute to one of those, follow its packaging docs and ignore this article's advice for that repository only. For anything you start today, create a module, commit go.mod and go.sum, and forget about src.

Also inModules
Share

Keep reading

Get new articles by email

We send one email when a new article is published. No spam.

Unsubscribe in one click. No spam, ever.