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 -modcacheempties it.$GOPATH/bin, wherego installwrites binaries. ThePATHline 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.