Go · 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
- Install VS Code and the Go extension (publisher:
golang.go). - Open a
.gofile. The extension offers to install its tools; accept. That installsgopls(the language server behind completion, inline errors and go-to-definition) anddlv(the debugger). - Turn on format-on-save, which runs
gofmt/goimportsevery 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
| Command | What 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 tidy | Add missing and remove unused requirements |
go get pkg@version | Add or upgrade a dependency |
go install pkg@latest | Build a tool into ~/go/bin |
go doc pkg.Symbol | Show 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.