Skip to main content
Go · lesson 2 of 25
Go beginner Lesson 2 of 25

Setting Up Go

Install Go and start a real module: go mod init, go run with arguments, go get, gofmt and go vet, and cross-compiling, each with its actual output.

By SkillByExample · Updated

Go installs as a single toolchain: one go command builds, runs, tests, formats and fetches dependencies. This lesson installs it, then walks through a real module, showing what each command printed.

Installing Go

Download the release for your platform from go.dev/dl. On Linux, extract it into /usr/local and put its bin directory on your PATH:

# Linux amd64; substitute the current version from go.dev/dl
wget https://go.dev/dl/go1.27.2.linux-amd64.tar.gz
sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.27.2.linux-amd64.tar.gz
echo 'export PATH=$PATH:/usr/local/go/bin:$HOME/go/bin' >> ~/.bashrc

On macOS and Windows, the .pkg and .msi installers from the same page set PATH for you. Then open a new terminal and check:

$ go version
go version go1.27.2 linux/amd64
$ go env GOPATH GOMODCACHE
/root/go
/root/go/pkg/mod

go version names the release and the platform it was built for. GOPATH defaults to ~/go (here /root/go, because this machine runs as root). You don’t put code there any more: Go uses it as a cache for downloaded modules (GOMODCACHE) and as the destination for go install, which writes executables to ~/go/bin — that is why the PATH line above adds it.

Creating a module

Before Go 1.11, every project had to live inside $GOPATH/src. Now a project lives anywhere, and a go.mod file at its root makes it a module:

$ mkdir myapp && cd myapp
$ go mod init github.com/yourname/myapp
go: creating new go.mod: module github.com/yourname/myapp
$ cat go.mod
module github.com/yourname/myapp

go 1.27.2

The module path is the prefix for every import inside the project (github.com/yourname/myapp/internal/db, for example). It only has to be a real repository URL if others will go get it. The go line records the minimum Go version the module needs; go mod init fills in the exact version of the toolchain that ran it, 1.27.2 here.

Your first program

Save this as main.go in myapp/. os.Args holds the command line, with the program name at index 0.

package main

import (
	"fmt"
	"os"
)

func main() {
	name := "World"
	if len(os.Args) > 1 {
		name = os.Args[1]
	}
	fmt.Printf("Hello, %s!\n", name)
}
$ cd myapp
$ go run .
Hello, World!
$ go run . Gopher
Hello, Gopher!
$ go build -o hello .
$ ./hello Alice
Hello, Alice!

go run . builds the package in the current directory and runs it, passing everything after the . to the program. go build -o hello . prints nothing on success and leaves an executable called hello; it runs on any Linux amd64 machine, Go installed or not.

Adding a dependency

go get downloads a module and records it in go.mod. Here the program uses github.com/google/uuid to derive a name-based UUID, which is the same on every run:

package main

import (
	"fmt"

	"github.com/google/uuid"
)

func main() {
	id := uuid.NewSHA1(uuid.NameSpaceDNS, []byte("go.dev"))
	fmt.Println(id, "version", id.Version())
}
$ go mod init example.com/dep
go: creating new go.mod: module example.com/dep
go: to add module requirements and sums:
	go mod tidy
$ go run .
main.go:6:2: no required module provides package github.com/google/uuid; to add it:
	go get github.com/google/uuid
$ go get github.com/google/uuid@v1.6.0
go: downloading github.com/google/uuid v1.6.0
go: added github.com/google/uuid v1.6.0
$ go run .
91da4ff0-bcdb-5fa4-b85d-7c2de6ebc8fe version VERSION_5
$ cat go.mod
module example.com/dep

go 1.27.2

require github.com/google/uuid v1.6.0 // indirect
$ go mod tidy
$ cat go.mod
module example.com/dep

go 1.27.2

require github.com/google/uuid v1.6.0
$ head -c 72 go.sum; echo
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+

The first go run fails: the import names a module that go.mod doesn’t require yet, and Go refuses to fetch it silently. go get downloads it into the module cache, adds a require line, and writes go.sum, which holds a checksum for each module so a tampered download is rejected next time. Commit both files.

Notice the // indirect comment: go get recorded the requirement without checking whether your code imports it directly. go mod tidy reads your imports, so it dropped the comment. It also adds anything imported but missing and removes requirements nothing imports any more — run it whenever imports change.

Formatting and vetting

gofmt settles code style for every Go project. This file is valid Go, but badly laid out — and has a bug:

package main
import "fmt"
func main(){
x:=42
    fmt.Printf("answer: %d %s\n",x)
}
$ gofmt -l .
main.go
$ go vet .
main.go:5:28: fmt.Printf format %s reads arg #2, but call has 1 arg
$ go run .
answer: 42 %!s(MISSING)
$ gofmt -w main.go
$ cat main.go
package main

import "fmt"

func main() {
	x := 42
	fmt.Printf("answer: %d %s\n", x)
}

gofmt -l lists files whose formatting differs from the standard; -w rewrites them in place (editors run this on save). Formatting cannot find the bug, but go vet does: the format string has two verbs and only one argument, and column 28 points at the %s with nothing to print. The program still compiles and runs — go vet catches what the compiler allows — and prints %!s(MISSING) where the second value should be. go test runs a subset of these vet checks automatically.

Cross-compilation

Set GOOS and GOARCH and the same toolchain builds for another platform, with nothing extra to install:

$ cd myapp
$ GOOS=windows GOARCH=amd64 go build -o app.exe .
$ GOOS=darwin GOARCH=arm64 go build -o app-mac .
$ file app.exe app-mac
app.exe: PE32+ executable for MS Windows 10.00 (console), x86-64, 16 sections
app-mac: Mach-O 64-bit arm64 executable, flags:<|DYLDLINK|PIE>
$ go tool dist list | wc -l
47

Both builds ran on a Linux machine. go tool dist list prints every supported GOOS/GOARCH pair, one per line. This works for pure Go code; a package that uses cgo (C code) would need a C cross-compiler for the target.

Setting up VS Code

  1. Install VS Code and the Go extension (publisher: golang.go).
  2. Open a .go file. The extension offers to install its tools; accept. That installs gopls (the language server behind completion, inline errors and go-to-definition) and dlv (the debugger).
  3. Turn on format-on-save, which runs gofmt/goimports every time you save:
{
  "editor.formatOnSave": true,
  "go.testFlags": ["-v", "-race"]
}

GoLand from JetBrains is the main alternative; Vim and Emacs users run gopls through their LSP clients.

Project layout

Go enforces no directory structure, but most projects converge on this one:

myapp/
├── go.mod
├── go.sum
├── main.go            # or cmd/<name>/main.go when there are several binaries
├── internal/          # importable only by code inside myapp
│   └── db/db.go
└── cmd/
    ├── server/main.go
    └── worker/main.go

internal/ is enforced by the compiler; the rest is convention. The packages lesson builds a multi-package module and shows the internal rule failing a build.

Essential commands

CommandWhat it does
go run .Compile and run the package in this directory
go build .Compile to an executable
go test ./...Run every package’s tests
go fmt ./...Rewrite source files in standard format
go vet ./...Report suspicious code the compiler accepts
go mod tidyAdd missing and remove unused requirements
go get pkg@versionAdd or upgrade a dependency
go install pkg@latestBuild a tool into ~/go/bin
go doc pkg.SymbolShow documentation from the terminal

go doc works offline from the source in your module cache and standard library:

$ go doc strings.ToUpper
package strings // import "strings"

func ToUpper(s string) string
    ToUpper returns s with all Unicode letters mapped to their upper case.

The Go Playground

go.dev/play compiles and runs a program in the browser — handy for sharing a snippet. It has no network or filesystem access and runs with a fixed clock (starting at 2009-11-10 23:00:00 UTC), so time-dependent output differs from a local run.

Practice

1. In myapp, make the program greet every argument instead of only the first, so go run . Ann Bo prints two lines.

Solution
package main

import (
	"fmt"
	"os"
)

func main() {
	names := os.Args[1:]
	if len(names) == 0 {
		names = []string{"World"}
	}
	for _, n := range names {
		fmt.Printf("Hello, %s!\n", n)
	}
}
$ go run . Ann Bo
Hello, Ann!
Hello, Bo!
$ go run .
Hello, World!

os.Args[1:] drops the program name; an empty slice falls back to the default.

2. Build myapp for Linux on 64-bit ARM (a Raspberry Pi 4 or an AWS Graviton instance) and confirm the architecture with file.

Solution
$ GOOS=linux GOARCH=arm64 go build -o app-arm64 .
$ file app-arm64
app-arm64: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked, Go BuildID=4OMsoOtEAU4ClY9OS72l/4Zb7RrNHuX9S6SQyqJyf/0IbjyiR5YH-dggsVLxO6/K2RYgys0foaRd_YR95Co, BuildID[sha1]=d37b46e32c4737e1f467e94e1e05b3002db74bdb, with debug_info, not stripped

ARM aarch64 is the 64-bit ARM architecture. You can’t run it on an amd64 machine, but you can copy it to one that is.

3. Introduce a go vet failure on purpose: in main.go, call fmt.Println("%d items", 3). What does go vet say, and does the program still run?

Solution
package main

import "fmt"

func main() {
	fmt.Println("%d items", 3)
}
$ go vet .
main.go:6:2: fmt.Println call has possible Printf formatting directive %d
$ go run .
%d items 3

Println doesn’t interpret format verbs, so %d is printed literally; vet recognises the mistake and suggests Printf. The build itself succeeds, which is why running go vet (or letting go test run it) matters.

4. Use go doc to find out what strings.Fields does without opening a browser.

Solution
$ go doc strings.Fields
package strings // import "strings"

func Fields(s string) []string
    Fields splits the string s around each instance of one or more consecutive
    white space characters, as defined by unicode.IsSpace, returning a slice
    of substrings of s or an empty slice if s contains only white space.
    Every element of the returned slice is non-empty. Unlike Split, leading and
    trailing runs of white space characters are discarded.

go doc reads the doc comment straight from the standard library source installed with Go.

Frequently Asked Questions

Do I still need to set GOPATH?
Not for most work. Since Go modules (introduced in Go 1.11 and default since 1.16), you can create projects anywhere on your filesystem. GOPATH still exists as a cache directory but you rarely need to think about it.
What is go.mod?
go.mod is the module definition file. It records the module path (used as the import base) and the minimum Go version required. It is created by go mod init and updated automatically by go get and go mod tidy.
Which editor should I use for Go?
VS Code with the official Go extension (backed by gopls) is the most popular choice. GoLand by JetBrains is the premium IDE option. Both provide autocompletion, inline errors, and integrated debugging.
What is go.sum and should I commit it?
go.sum records a cryptographic checksum for every module version your build depends on, so a later download that does not match is rejected. Commit it alongside go.mod; go mod tidy keeps both files in sync with your imports.
How do I build a Go binary for another operating system?
Set the GOOS and GOARCH environment variables for the build, for example GOOS=windows GOARCH=amd64 go build. The standard Go toolchain cross-compiles pure Go code without any extra compiler; only code using cgo needs a C toolchain for the target.