Skip to content
CalliCoder

Go Installation and Getting Started Guide

Published Updated Golang 11 min read

Installing Go and starting a project with modules — what GOPATH is still for, why you no longer need to work inside it, and the environment variables worth knowing about.

Older Go guides spend most of their length on GOPATH: a single directory tree that all your code had to live inside, with imports derived from the path on disk. That requirement is gone. Modules replaced it in Go 1.11 and became the only mode in 1.16.

GOPATH still exists and still does two useful things, which is why the confusion persists. This covers the current setup, and says plainly what GOPATH is and is not for.

Written against Go 1.22.

Install

Linux: take the tarball rather than the distribution package, which is often several versions behind:

$ curl -LO https://go.dev/dl/go1.22.5.linux-amd64.tar.gz
$ sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.22.5.linux-amd64.tar.gz

Then add it to PATH in ~/.profile or ~/.bashrc:

export PATH=$PATH:/usr/local/go/bin
export PATH=$PATH:$(go env GOPATH)/bin

sudo rm -rf /usr/local/go before extracting is in the official instructions for a reason: the archive does not remove old files, and mixing two versions in one directory produces confusing build errors.

macOS, brew install go, or the pkg installer from go.dev.

Windows, the msi installer, which sets PATH for you.

Verify:

$ go version
go version go1.22.5 linux/amd64

The second PATH line matters

go install puts binaries in $(go env GOPATH)/bin, which is ~/go/bin by default. Without that directory on PATH, every tool you install is invisible:

$ go install golang.org/x/tools/cmd/goimports@latest
$ goimports -h
goimports: command not found        # installed fine, not on PATH

That is the most common “I installed it and it does not work” in Go, and it is not a Go problem.

Start a project anywhere

$ mkdir ~/projects/hello && cd ~/projects/hello
$ go mod init github.com/yourname/hello
go: creating new go.mod: module github.com/yourname/hello

Note the directory: ~/projects, not ~/go/src. With modules, a project lives wherever you want it.

The module path is an identity, not a filesystem path. Use the repository URL you would publish under: github.com/yourname/hello — even for something private, because that is what other modules would import. A bare name like hello works locally and cannot be imported by anything else.

// main.go
package main

import "fmt"

func main() {
    fmt.Println("hello")
}
$ go run .
hello

$ go build -o hello .
$ ./hello
hello

Adding a dependency

$ go get github.com/google/uuid
go: downloading github.com/google/uuid v1.6.0
go: added github.com/google/uuid v1.6.0

Two files now describe the build:

go.mod   the module path, the Go version, and direct + indirect requirements
go.sum   cryptographic hashes of every module version used

Commit both. go.sum is not a lock file in the npm sense: go.mod already pins exact versions — it is a record of expected hashes, so a tampered or changed dependency fails verification rather than building silently.

Everyday commands:

$ go mod tidy          # add what is imported, remove what is not
$ go get -u ./...      # upgrade dependencies
$ go list -m all       # what is actually in the build
$ go mod why <module>  # why is this in my build?
$ go mod download      # populate the cache without building

go mod tidy before every commit. It is the difference between a go.mod that describes the code and one that accumulates entries nobody uses.

What GOPATH is still for

$ go env GOPATH
/home/you/go

Two things, neither of which is where your source lives:

  • $GOPATH/pkg/mod, the module cache. Every downloaded dependency, shared across all your projects. It grows; go clean -modcache empties it.
  • $GOPATH/bin, where go install writes binaries. The PATH line above.

You can still write code inside $GOPATH/src and it will build. There is no advantage, and it recreates a constraint the toolchain removed.

Environment variables worth knowing

$ go env                        # everything
$ go env GOOS GOARCH            # linux amd64
GOOS / GOARCH       target platform GOOS=linux GOARCH=arm64 go build cross-compiles
CGO_ENABLED         0 for a static binary with no libc dependency
GOPROXY             module proxy; proxy.golang.org by default
GOPRIVATE           patterns to fetch directly, bypassing proxy and checksum database
GOFLAGS             flags applied to every go command, e.g. -mod=readonly
GOMODCACHE          overrides $GOPATH/pkg/mod

Cross-compilation is genuinely one command, which is one of Go’s better properties:

$ GOOS=darwin GOARCH=arm64 go build -o hello-mac .
$ GOOS=windows GOARCH=amd64 go build -o hello.exe .

GOPRIVATE is the one that matters in a company. Without it, go get on a private repository asks the public proxy and the public checksum database about a module they cannot see, and the failure looks like a network problem:

$ go env -w GOPRIVATE=github.com/yourcompany/*

go env -w persists settings to a config file rather than your shell profile, which is usually what you want.

Project layout

For a single binary, flat is correct:

hello/
├── go.mod
├── go.sum
├── main.go
└── greet/
    └── greet.go

Once there is more than one binary, the convention is cmd/:

notes/
├── go.mod
├── cmd/
│   ├── server/main.go
│   └── migrate/main.go
├── internal/
│   ├── store/
│   └── http/
└── pkg/
    └── client/

internal/ is enforced by the compiler: a package under internal/ can only be imported by code rooted at its parent. That is the one directory name with real meaning, and it is the right default for anything you are not committing to as a public API.

pkg/ is convention only and somewhat contested. A flat layout with well-named packages is fine and often better: resist creating directories for their own sake, and see packages for what actually belongs in one.

Working on two modules at once

The one case where the old single-tree layout was genuinely convenient: editing a library and the service that uses it, together. Modules replaced that with replace directives, which worked and had to be removed before committing, and were regularly forgotten.

Go 1.18 added workspaces, which keep the override outside the modules entirely:

$ mkdir ~/projects/work && cd ~/projects/work
$ git clone [email protected]:you/notes-api && git clone [email protected]:you/notes-client
$ go work init ./notes-api ./notes-client
# go.work
go 1.22

use (
    ./notes-api
    ./notes-client
)

Any build run from the workspace root resolves notes-client to the local checkout rather than the published version, without either go.mod changing. Add go.work and go.work.sum to .gitignore, a workspace describes one developer’s machine, not the project.

go work use ./another adds a module; GOWORK=off disables the workspace for one command, which is how you check that a build still works the way CI will run it.

Tooling

Everything below ships with Go:

$ go fmt ./...          # formatting is not a matter of opinion
$ go vet ./...          # correctness checks: printf args, unused results, lock copies
$ go test ./...         # tests
$ go test -race ./...   # with the race detector
$ go doc fmt.Println    # documentation, offline

gofmt output is the format. There is no configuration and no style debate, which is a feature.

go vet before every commit: it catches a mismatched Printf verb, a copied mutex, an unused result from fmt.Errorf. These compile fine and are all bugs.

For editing, the official extension for VS Code, or GoLand. Both use gopls, the language server, which the extension installs on first run.

Frequently asked questions

Do I still need to set GOPATH?

No. It defaults to ~/go and modules mean your code lives wherever you like. You do need $(go env GOPATH)/bin on PATH for installed tools.

Where should my project live?

Anywhere. ~/projects/whatever is fine; $GOPATH/src offers nothing any more.

What should the module path be?

The repository URL you would publish under, such as github.com/you/project: even privately, since that is how other modules would import it.

Why is my installed tool “command not found”?

go install writes to $(go env GOPATH)/bin, which is not on PATH by default.

Should I commit go.sum?

Yes, both files. go.sum records expected hashes so a changed dependency fails verification instead of building quietly.

What is the difference between go get and go install?

go get adds or updates a dependency of the current module. go install pkg@version builds and installs a binary and does not touch your go.mod.

What does go mod tidy do?

Adds requirements for everything imported and removes everything not imported. Run it before committing.

How do I use a private module?

Set GOPRIVATE to a matching pattern so the toolchain skips the public proxy and checksum database. Without it the failure looks like a network error.

How do I cross-compile?

Set GOOS and GOARCH on the build command. Add CGO_ENABLED=0 for a fully static binary.

Do I need a pkg/ directory?

No. internal/ is enforced by the compiler and worth using; pkg/ is convention only, and a flat layout with good package names is frequently better.

Where should I go next?

Packages covers how to organise code inside a module, and basic types covers the language itself.