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.

DirectoryPurpose
srcLocation 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.
pkgLocation of compiled package code (i.e., .a). For example, when you run go install, you can use it in your code.
binLocation 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.