I’m assuming you are in the stage of learning Go. Therefore, you should have Go installed on your machine. If not, please refer to the Appendix section for installation links.
Before we get started, I need to get you on the same page. Throughout this entire article, I will be using the go version 1.18.3. So, I’d recommend you either use the version that I use, version 1.11, or any above to grasp everything properly.
What is GOROOT?
GOROOT is the variable that defines where Go SDK is located. This is where Go’s code, compiler, and rest of the required tooling lives. This folder does not hold our source code. The $GOROOT is something similar to /usr/local/go or /opt/homebrew/Cellar/go/1.X.X/bin.
In older versions of Go, we set $GOROOT manually. But in newer versions, we don’t need to set up the $GOROOT variable unless you use different Go versions. If you install Go in a different path, then export the variable $GOROOT in your shell’s default profile (i.e., .zshrc, .profile).
What is GOPATH?
To explain this, we need to travel back in time. Let’s see how things were before Go 1.11.
How old GOPATH works
When Go was first introduced in 2009, Go authors required you to organize the Go code specifically when working with the go tool. A convention to say. Here’s some simplified information I borrowed from the docs.
Go programmers typically keep all their Go codes in a single workspace.
A workspace contains many version control repositories (i.e., managed by Git, Bitbucket, etc.).
Each repository contains one or more packages.
Each package consists of Go source files in a single directory.
The path to a package’s directory determines its import path.
Go authors had this notion called a single workspace directory. It is very different from other programming language environments (i.e., C++) in which the project has a separate workspace and can be multiple workspaces closely tied to version-controlled repositories.
Location of your Go source code i.e., .go, .c, .g, .s. The src subdirectory typically contains multiple version control repositories (such as for Git or Mercurial) that track the development of one or more source packages.
pkg
Location of compiled package code (i.e., .a). For example, when you run go install, you can use it in your code.
bin
Location of compiled executable programs built by Go. The go tool builds and installs binaries to this directory.
To give a rough idea of how a workspace looks in practice, here’s an example:
Well, now let’s take this convention into practice and understand how it was before back then.
HolUp — Wait a Minute
From this point onwards, we will look at how GOPATH used to be in earlier versions before 1.11. And for that, we need to turn off module-aware mode from environment variables. You can check whether you have it enabled or disabled by using the following command.
go env GO111MODULE
This will show the value of the GO111MODULE variable.
If you don’t see the value as off, I want you to overwrite the default value by executing go env -w GO111MODULE=off to explicitly turn off the module-aware mode. Now we can make sure that we use the semantics before 1.11.
By default, Go sets auto, "" (empty string), or on as values of GO111MODULE depending on the version you are using.
Sometimes GO111MODULE environment variable is a pain in the arse. Gophers rely on this environment variable to change how Go imports packages. As I said, it depends on the version you are using. Go and its module-aware mode semantics change with different Go versions (well, slightly).
GO111MODULE in Go 1.11 and 1.12
Value
Behaviour
on
This enforces Go to use modules even if the project is in your $GOPATH/src. It strictly requires go.mod to work.
off
This enforces Go to behave the old way (the $GOPATH way). Even if you place the project outside $GOPATH it will always require the project to be in $GOPATH/src (well kinda. I’ll explain a way around this).
auto (default)
This is the default mode. In this mode, Go will behave: - 1) Similar to on when you are outside $GOPATH. 2) Similar to off when you are inside the $GOPATH even if a go.mod is present.
GO111MODULE in Go 1.13
Value
Behaviour
on
Same as 1.11 and 1.12
off
Same as 1.11 and 1.12
auto (default)
In this mode, Go will behave: - 1) Behaves like GO111MODULE=on anywhere if there is a go.mod OR anywhere outside the GOPATH even if there is no go.mod. This way you can keep all your repositories in your GOPATH with Go 1.13. 2) Behaves like GO111MODULE=off in the GOPATH if there’s no go.mod.
GO111MODULE in Go 1.14 and 1.15
Has the same semantics as Go version 1.13.
GO111MODULE in Go 1.16
Value
Behaviour
on (default)
This will force using Go modules regardless of whether a go.mod file is present in the current working directory or a parent directory.
off
Same as the previous version.
auto
Same as the previous version.
In this version, if you see GO111MODULE as GO111MODULE="" or GO111MODULE= as an empty string or empty value it simply means GO111MODULE=on. I found out this via a issue comment on golang repo. It applies to both 1.15, 1.16 and beyond.
GO111MODULE in Go 1.17
Has the same semantics as Go version 1.16.
GO111MODULE in Go 1.18
Has the same semantics as Go version 1.17.
However, now go get no longer builds or installs packages in module-aware mode. go get is now dedicated to adjusting dependencies in go.mod. Effectively, the -d flag is always enabled by default. Please refer the go cmdchangelist in the 1.18 release notes for more information.
Now that Go module-aware mode is disabled, the packages we develop and install should be in $GOPATH so that the Go build system knows where the imported packages are.
Now go tool expects you to keep your project and source files in GOPATH/src. And go-tool uses pkg/ for compiled packages and the bin/ for executables. This gives you all the necessary files for your development, and go-tool can resolve packages you have imported into your project.
Why learn if it’s obsolete?
Well, I’m glad you asked! Frankly, you don’t need to. Especially if you were a Gopher from the very beginning. But if you’re just starting out, you might run into slight hiccups here and there from time to time because we still have content that refers to older Go $GOPATH behavior. So, it’s always better to know the history.
Okay, now let’s see an example.
We can import third-party packages (i.e., libraries written Go, C, or even C++) or our own custom packages to our programs. For example, consider the below application.
Let’s say we want to create a calculator application. And along the way, publish our calculator operations as a reusable module so that other developers can reuse them. And make the interface and the logic separately in a different package as a driver application. And driver uses a third-party package called chalk to change the output colors.
Sounds easy, right? Let’s see how we can do that.
mkdir -p $GOPATH/src/operations # create directorycd $GOPATH/src/operations # navigate to the directorytouch operations.go # create package file
Open the operations.go file in your editor and paste in the following source.
operations.go
package operationsfunc Add(a int, b int) int { return a + b}func Sub(a int, b int) int { return a - b}
Okay, we have already created our reusable operations above. But how can we practically compile and use it in other projects?
Well, obviously we need another package to do so. But first, we need to execute go install inside the $GOPATH/src/operations package to create a compiled binary to use in other applications.
cd $GOPATH/src/operations && go install
If you navigate to $GOPATH/pkg, you will see that operations.a compiled binary file will be generated in $GOPATH/pkg/{GOOS}_${GOARCH} directory.
Now that we have a binary file, we can actually go ahead and create a new application package called calcapp1.
mkdir -p $GOPATH/src/calcapp # create directorycd $GOPATH/src/calcapp # navigate to application directorytouch main.go # create driver main file
Remember that we want the third-party library called chalk to format our output. So let’s go ahead and install that too.
go get github.com/ttacon/chalk
You can execute this command from anywhere in your machine as long as you have $GOPATH in your environment variables.
There is quite a bit happening in the background. First, without Go 11 modules enabled, the package we get from go get should be in $GOPATH so that the Go build system knows where the imported packages are.
First, Go fetches the package chalk and then puts its source under $GOPATH/src in a domain/org/package manner. And it installs the package and places its compiled binary in $GOPATH/pkg/${GOOS}_${GOARCH} in the same way as we talked about.
With this, we explored all subdirectories src/, pkg/, and bin/ in the root workspace directory. Well, that’s not it. We have a couple of more things left to learn about the old $GOPATH.
Can we place our project outside $GOPATH?
When I started learning Go back in 2017, I didn’t put my go files in $GOPATH. My dumbass didn’t refer the docs properly. But it turns out, it strangely compiled the code, and it worked for one main package. Even when GO111MODULE is disabled or in older versions of Go, we can place our projects outside the go path. This is what I mean:
Here’s a working example
I’m implementing a project named myproject, with a package main and including two files main.go and some_functions.go as follows:
$ cd $HOME/Desktop/myproject && go run .hello world!
And voilà! Surprisingly, the program runs, even when the project is outside $GOPATH. Actually, all projects must be in $GOPATH is due to sub-packages. In the above example, we only had the main package. So, it makes sense why it worked. And one main package is not the case, so it is not confined by this limitation. But when you add another package to the project, things get a bit fussy.
Go is unable to resolve the package by its relative path or package name.
Without the Go module feature enabled, we cannot specify our functions/ package in the main package, so we cannot find out which should be in the import statement “function” location and cannot build this project. The only way to make it work without Go modules was to move the project into $GOPATH/src like so:
When you move the project inside GOPATH/src go is able to resolve the package by its relative path.
This was a strictly opinionated approach back in the day, and every Gopher had to follow this convention and maintain their source like this back in the day. Conceptually, this allowed Gophers to link any go code at any instant of time without ambiguity. Well seems pretty reasonable, isn’t it? But, NO!.
Well, what’s the problem then?
The above $GOPATH approach worked well for a cohesive, more extensive monorepos that doesn’t rely on third-party packages2.
Imagine you have semantic versioning in third-party packages and including your own. So with time, it will get hairy real quick. Without any versioning for the packages, it led to all this pain of managing different tags in the source.
Because of this, Go authors decided to introduce the GO111MODULE environment variable to handle it. Before Go 1.11, authors didn’t consider shipping the go tool with a package manager. Instead, we used go get to fetch all the sources by using their repository import path and placing them in $GOPATH/src. Since there was no package manager or any versioning, the master branch would represent a stable version of the package.
You might see references to Go Modules as vgo. Basically, it means the same thing. And it implies Versioned Go. Instead of using the $GOPATH for storing a single git checkout of every package, Go Modules stores tagged versions with go.mod keeping track of each package’s version.
However, Go runtime still does use $GOPATH as a download directory for Go packages. To make Google’s saying correct, the Go module does not entirely replace $GOPATH. The go tool uses it for package distribution and versioning. The main goal of go modules is to distribute modules in a much more streamlined way. And now we are no longer confined to GOPATH. So, placing sources under src/ folder is ineffective and is not the best practice when you have module-aware mode enabled.
The interaction between the GOPATH behavior and the Go Modules behavior has become one of Go’s most significant turning points. Finally, now we can learn how it works in practice.
How GOPATH + GO111MODULE works
Now it’s time to see how GOPATH works with Go Modules. I’ll give you a similar example we tried above and modify it to cover the things I mention below.
How to import locally created modules into a project.
How to use remote modules installed via go get.
How to use module sub-packages.
Switch to Go Modules
From this point onwards, we are using the modern approach. We are going to turn on GO111MODULE from environment variables.
go env -w GO111MODULE=on
Also, to increase the clarity of the tutorial, I will be cleaning up my $GOPATH and previous examples from my workspace.
sudo rm -rf ~/Desktop/myproject $GOPATH/* && cd $GOPATH && mkdir -p src pkg bin
Use this command wisely. Don't run this if you have any other projects in your $GOPATH.
Go ahead and create a workspace anywhere3. First, let’s start off with the operations package.
DIR="$HOME/Desktop/go_workspace" # My root workspacemkdir -p $DIR/operations && cd $DIR/operations # Create package directory.go mod init operations # Initialize go module.touch $DIR/operations/operations.go # Create the source file.
Paste the following source in operations.go.
operations.go
package operationsfunc Add(a int, b int) int { return a + b}func Sub(a int, b int) int { return a - b}
Then, let’s write the driver application calcapp.
mkdir -p $DIR/calcapp $DIR/calcapp/formatters # Create package directory.cd $DIR/calcapp && go mod init calcapp # Initialize go module.touch $DIR/calcapp/formatters/formatters.go $DIR/calcapp/main.go # Create source files.
Note that we create a subpackage named calcapp/formatters.
Also, for the driver application, we need a third-party package called chalk.
go install github.com/ttacon/chalk # Installs the chalk package.go mod tidy # Sync sums
Remember, with go 1.18, go get no longer builds or installs packages in module-aware mode. So we install it!
Now notice that we have a custom local package called operations. We need to import that package into our calcapp to make it work. So what do we do? To point to the local version of a dependency in Go rather than the one over the web, we use the replace keyword within the go.mod file.
The replace line goes above your require statements, like so:
calcapp/go.mod
module calcappgo 1.17// Can be github.com/yourname/operationsreplace yasint.dev/operations => ../operations// 💡 The actual semantic version hash will be different in yours.require github.com/ttacon/chalk v0.0.0-20160626202418-22c06c80ed31require yasint.dev/operations v0.0.0
And now, when you compile calcapp module using go install, it will use your local code rather than resolve a non-existing web dependency.
Did you notice? We can directly import our package by a path like calcapp/formatters and even reference local modules effortlessly! How cool is that? Go mod is more intelligent than this. It can even recursively resolve multiple nested sub-packages.
cd ~/Desktop/go_workspace/calcappgo installcalcapp -a 10 -b 10 # => Addition: 20calcapp -sub -a 10 -b 5 # => Subtraction: 5
Install the program and run it!
Conclusion
Now we know with Go module-aware mode enabled, Go projects are no longer confined to $GOPATH. Meaning Go never restricts the structure or the location of Go projects. Go module alleviates versioning and module resolution constraints elegantly. I hope now you have a better understanding of $GOPATH and $GOROOT.
Thanks for reading! Until next time!
Appendix
macOS installation (Apple Silicon)
We can use homebrew to install golang. It will take care which binaries should be installed for different cpu architectures.
Install Golang
brew install golang
This command will install golang via homebrew. It might take a few minutes depending on your network connection's speed.
Check installation path
Once you installed, brew has a command that you can check where it exactly installed. By executing brew info golang you will get a similar output like the following.
go: stable 1.17.5 (bottled), HEADOpen source programming language to build simple/reliable/efficient softwarehttps://go.dev//opt/homebrew/Cellar/go/1.17.5 (10,825 files, 564.3MB) *...
Highlighted line is the installation path.
Verify go
Check whether the go command is working properly to verify you have it in the path.
go version # Outputs => go version go1.17.5 darwin/arm64
Tip 💡: Execute command -v go to check the command’s path.
Scrumptious bits
Usually when you install a package via brew, it will place the binaries in /opt/homebrew/Cellar/. Then after every folder under that is symlinked to /opt/homebrew/opt and, the go command will be automatically symlinked via /opt/homebrew/bin.
In Apple Silicon based macs we have to append the /opt/homebrew/bin to your $PATH variable to work everything correctly (Kurt B, Stackoverflow).
Usually you don’t have to worry about things happening inside /pkg directory. It’s entirely handled by go tool. On a high-level note, know that Go uses it to keep compiled binaries and build caches. ↩