How GOROOT and GOPATH Works
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.
But in Golang, a workspace refers to a directory hierarchy with three directories at its root.
Directory | Purpose |
---|---|
src | 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.
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.
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.
Open the operations.go
file in your editor and paste in the following source.
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.
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 calcapp
*.
Remember that we want the third-party library called chalk to format our output. So let’s go ahead and install that too.
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.
Now we can start writing our driver application.
Now we can run the driver application by executing go run main.go
and see the output.
Now, if you execute go install
and check /bin
, you can see that the executable is put. Navigate to
$GOPATH/src/calcapp
and execute the following.
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 dumbazz
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: -
And run it like this: -
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.
Here's a Problematic Example
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: -
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 packages*.
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.
When Google released Go 1.11
, they said: This release adds preliminary support
for a new concept called "modules," an alternative to GOPATH
with integrated support for
versioning and package distribution. Go 1.11
was released
on August 24, 2018.
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.
Go ahead and create a workspace anywhere*. First,
let's start off with the operations
package.
Paste the following source in operations.go
.
Then, let's write the driver application calcapp
.
Also, for the driver application, we need a third-party package called chalk
.
Paste the following source in formatters.go
.
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: -
And now, when you compile calcapp
module using go install
, it will use your local code rather
than resolve a non-existing web dependency.
We can safely paste the following source in main.go
.
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.
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
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.
Verify go
Check whether the go
command is working properly to verify you have it in the path.
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).
Other Installations
Well, for other systems official Golang installation guide is far more than enough.
Well, now what?
You can navigate to more writings from here. Connect with me on LinkedIn for a chat.
2023
December
October
August
June
May
March
January