Getting Started

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Go 1.27 (go1.27.2, released 2026-10-08) and was checked on 2026-10-18. It gets a working toolchain onto your machine, compiles a first program in several ways, builds a small multi-package module around the shelf library-catalogue scenario used throughout this section, calls a package from a second module, and shows how to read what the compiler and go vet tell you. The next page, Toolchain Versions and Compatibility, explains the release cycle, the compatibility promise and how the go command chooses a toolchain.

Every program on this page was compiled and run with go1.27.2 on darwin/arm64; outputs that mention the platform (a binary size, a file description) differ on other systems, and the program text is the same everywhere.

What Go is for

Go is a compiled, statically typed, garbage-collected language. It was designed to make large software projects and their build systems less painful: fast builds, a small language that is quick to read, first-class support for concurrency on multicore machines, and tooling (formatting, documentation, testing, dependency management) that ships with the language and works the same way for everyone. The Go FAQ describes the motivation as frustration with the complexity of building big systems in the languages of 2007, together with the need for safe, simple concurrency and automatic memory management.

Domain Typical uses

Cloud services and APIs

HTTP and gRPC back ends, microservices and proxies built with net/http and log/slog (see net/http Clients and Servers)

Infrastructure and DevOps tooling

container runtimes, orchestrators, infrastructure-as-code tools and CI helpers

Command-line tools

single static binaries that are easy to copy and cross-compile (see Command-Line Programs and CLI frameworks in Go and Rust)

Networking and concurrent systems

servers that hold many connections, pipelines, crawlers, message brokers (see Goroutines and the Scheduler)

Data and text processing

log processors, ETL jobs, template-driven generators (see Text and HTML Templates)

Go is a deliberately small language. If you know Rust, compare this page with Getting Started in the Rust Reference: both ship one tool that builds, tests, formats and manages dependencies, but Go trades Rust’s compile-time ownership checking for a garbage collector and a much shorter list of features. Readers coming from C will find the same "compile to a native binary" workflow as in Getting Started in the C Reference, with a module system instead of Makefiles and header files.

The project and how Go evolves

The language started as a whiteboard sketch by Robert Griesemer, Rob Pike and Ken Thompson on 2007-09-21 and became a public open source project on 2009-11-10. Go 1, the version the compatibility promise is anchored to, defines the language specification and the core standard library; every release since then adds features without breaking Go 1 programs. Releases ship every six months (February and August), each major release is supported until two newer ones exist, and the schedule is described in Toolchain Versions and Compatibility.

Changes to the language, the libraries and the tools go through the proposal process: anyone files an issue in the golang/go repository, the proposal review group discusses it, and accepted proposals are implemented and listed in the release notes. The FAQ is blunt that very few language changes are accepted, because the compatibility promise rules out anything that would break existing programs. Long-debated features do land eventually: type parameters arrived in Go 1.18, range-over-func iterators in Go 1.23 and generic methods in Go 1.27.

The sources you will use again and again are the language specification, the release notes, the standard-library documentation on pkg.go.dev, the blog and the wiki; they are collected under == References on every page of this section.

Source: Go FAQ: Origins

Installing Go

Download the archive or installer for your platform from the official download page. A Go installation is a single directory tree (GOROOT) containing the go command, the compiler, linker and other tools under pkg/tool, and the standard library sources. There is no separate runtime to install: compiled programs carry their own.

Linux

Remove any previous installation, then extract the archive into /usr/local, which creates a fresh /usr/local/go. Never extract over an existing tree.

rm -rf /usr/local/go && tar -C /usr/local -xzf go1.27.2.linux-amd64.tar.gz
export PATH=$PATH:/usr/local/go/bin
go version

Add the export line to $HOME/.profile (or /etc/profile for all users) so that it survives new sessions. This procedure needs root, so run each command with sudo if you are not root.

macOS

Open the .pkg file and follow the prompts. It installs the tree into /usr/local/go and adds /usr/local/go/bin to PATH through a file in /etc/paths.d. Open a new terminal afterwards so the change takes effect.

Windows

Open the .msi file and follow the prompts. The default location is under Program Files, and the installer updates the PATH that new Command Prompt and PowerShell windows inherit, so close and reopen any open window before testing.

Check the installation and add the install directory to PATH

$ go version
go version go1.27.2 darwin/arm64

Programs installed with go install land in $(go env GOPATH)/bin ($HOME/go/bin by default), so add that directory to PATH too:

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

Package-manager builds of Go (Homebrew, apt and others) exist, but they can lag behind the official release; the official archive is the reference. To run several Go versions side by side you do not install them by hand any more: put the version you want in the go or toolchain line of go.mod and the go command fetches and caches it (see Toolchain Versions and Compatibility).

The go command and its environment

go is the single entry point for everything: building, running, testing, formatting, documentation, dependency management and diagnostics. go help prints the command list; these are the commands in go1.27.2:

Command What it does

go build

compile packages and their dependencies; for a main package, write an executable

go run

compile and run a main package, then delete the executable

go install

compile and install packages or commands; executables go to GOBIN

go test

run tests, benchmarks, fuzz targets and examples (see Testing)

go vet

report likely mistakes in packages (see Formatting, vet, fix and Static Analysis)

go fmt

run gofmt on package sources

go fix

apply fixes suggested by static analyzers, including the modernizers

go doc

show documentation for a package or symbol

go list

list packages or modules, with a template language for scripting

go mod

module maintenance: init, tidy, edit, vendor, why, graph

go get

add or upgrade dependencies of the current module

go work

workspace maintenance

go generate

process //go:generate directives (see Directives, Code Generation and Embedding)

go clean

remove object files and cached files

go env

print or change Go environment information

go version

print the toolchain version, or the Go version a binary was built with

go tool

run a specific tool such as compile, link, pprof, trace, cover or objdump

go telemetry

manage Go telemetry settings

go bug

start a bug report

go help <command> documents one command; go help also has topic pages such as buildconstraint, buildmode, cache, environment, modules, module-auth, packages and testflag.

Source: cmd/go

go env: reading and changing the environment

Go keeps configuration in environment variables, but with a twist: go env -w stores a value in a per-user file so you do not have to export it in every shell. The lookup order is the process environment first, then the user file, then $GOROOT/go.env, which carries the defaults GOPROXY=https://proxy.golang.org,direct, GOSUMDB=sum.golang.org and GOTOOLCHAIN=auto.

$ go env GOROOT GOPATH GOMODCACHE GOBIN GOOS GOARCH CGO_ENABLED GOFLAGS
~/sdk/go1.27.2
~/go
~/go/pkg/mod
~/go/bin
darwin
arm64
1


$ go env -json GOOS GOARCH
{
	"GOARCH": "arm64",
	"GOOS": "darwin"
}

(The home directory is shortened to ~; the empty last line is GOFLAGS, which is unset.) go env with no arguments prints every setting as KEY='value'; -json prints JSON; -changed prints only the settings whose values differ from the defaults; -w and -u write and remove entries in the user file. A sandboxed demonstration, with GOENV pointing at a scratch file so nothing real is touched:

$ go env -w GOTOOLCHAIN=go1.27.2+auto GOFLAGS=-mod=readonly
$ cat "$GOENV"
GOFLAGS=-mod=readonly
GOTOOLCHAIN=go1.27.2+auto
$ go env -changed | grep -E 'GOFLAGS|GOTOOLCHAIN'
GOFLAGS='-mod=readonly'
GOTOOLCHAIN='go1.27.2+auto'
$ go env -u GOFLAGS
$ go env -changed | grep -E 'GOFLAGS|GOTOOLCHAIN'
GOTOOLCHAIN='go1.27.2+auto'

The location of the user file comes from go env GOENV (os.UserConfigDir() plus go/env, for example ~/.config/go/env on Linux). A value in the process environment always wins over the file, and go env -w warns when it would be overridden.

The variables you will meet first:

Variable Meaning

GOROOT

root of the Go installation; the go command infers it, so do not set it by hand

GOPATH

user workspace for downloaded module cache and installed tools; default $HOME/go; no longer where your code lives

GOMODCACHE

where downloaded modules are stored, default $GOPATH/pkg/mod

GOCACHE

build and test cache, default under the user cache directory

GOBIN

install directory for go install; default $GOPATH/bin

GOFLAGS

default flags for every go command, for example -mod=readonly

GOPROXY, GOSUMDB, GOPRIVATE

module download and checksum settings (see Modules and Dependency Management)

GOTOOLCHAIN

which toolchain the go command runs; see Toolchain Versions and Compatibility

GOOS, GOARCH

target platform; default to the host (see The go Command, Builds and Cross-Compilation)

CGO_ENABLED

1 when C code may be compiled in (see cgo and C Interoperability)

GOWORK

path to the active go.work file, or off

GODEBUG, GOEXPERIMENT

runtime and compatibility switches; see the toolchain page

GOTELEMETRY

read-only: the telemetry mode, local by default

Editor setup

See Go IDEs & Tools for a list of IDEs and editors.

You can write Go in any text editor, but the experience is much better with the official language server, gopls, which the Go team maintains. It gives diagnostics as you type, completion, go-to-definition, find references, rename, code actions (including go fix modernizers) and organizing imports, in any editor that speaks the Language Server Protocol. Install it once; most editor plugins do this for you:

go install golang.org/x/tools/gopls@latest

This command downloads from the network, so it is not run in the sandbox that produced the outputs on this page; gopls@v0.24.0 is the release current on 2026-10-18.

Editor Setup

Visual Studio Code

install the Go extension; it installs and updates gopls and the Delve debugger

GoLand and IntelliJ IDEA Ultimate

Go support is built in or a plugin; the JetBrains IDE uses its own analysis rather than gopls

Vim and Neovim

the vim-go plugin, or Neovim’s built-in LSP client pointed at gopls

Emacs, Helix, Sublime Text and others

any LSP client works; the gopls documentation lists configuration for each

Two conventions are worth setting up on day one: format on save (the editor runs gofmt, or gopls does it for you, so formatting never shows up in code review) and organize imports on save (gopls adds and removes import lines). The debugger of choice is Delve (dlv), which understands goroutines and is what the editor integrations drive.

Hello, world!

A Go program lives in a module: a directory tree with a go.mod file at its root that names the module and records the Go version and dependencies. The code inside is organized in packages, one per directory. A package named main that declares a function main builds into an executable.

$ mkdir shelf && cd shelf
$ go mod init example.com/shelf
go: creating new go.mod: module example.com/shelf

go mod init takes the module path, which should be the place the module will be downloadable from (a repository URL without the scheme). The go line it writes is the toolchain’s own version: with go1.27.2 the file says go 1.27.2. The Go 1.26 release notes announced that go mod init would write the version of the previous release, so check what your toolchain writes. Either way, you set the minimum Go version explicitly with go mod edit -go=1.27 or, when the toolchain can be downloaded, go get go@1.27. The go.mod file for the rest of this page:

module example.com/shelf

go 1.27

Now the program, in main.go:

package main

import "fmt"

func main() {
	fmt.Println("shelf: a library catalogue")
}

Three commands turn it into something you can run:

$ go run .
shelf: a library catalogue

$ go build
$ ls -l shelf | awk '{print $5, $9}'
2429714 shelf
$ ./shelf
shelf: a library catalogue

$ go install .
$ ls "$(go env GOBIN)"
shelf
Command Behaviour

go run .

builds the package in the current directory into a temporary location, runs it and removes it; arguments after the package go to the program

go build

compiles and links; for a main package it writes the executable into the current directory, named after the last element of the module path (or the directory); for a non-main package it only checks that it compiles

go build -o bin/shelf-cli .

same, but with the output name and location you choose (-o may also be a directory)

go install .

builds and copies the executable to GOBIN (by default $GOPATH/bin, which should be on PATH)

go install pkg@version

downloads, builds and installs a command from a module version, ignoring any go.mod in the current directory; this is how tools such as gopls are installed

(GOBIN is set explicitly in the sandbox that produced this output; by default go install writes to $GOPATH/bin.)

go run accepts several forms of the package argument:

$ go run main.go            # a list of .go files, even without a go.mod
shelf: a library catalogue
$ go run .                  # the package in the current directory
shelf: a library catalogue
$ go run example.com/shelf  # an import path inside the main module
shelf: a library catalogue

Exit status, arguments and the environment

go run is a build tool, so it reports the exit status of your program in its own words, and exits with status 1. When you need the real status, build and run the binary yourself.

package main

import (
	"fmt"
	"os"
	"runtime"
)

func main() {
	fmt.Println(runtime.Version(), runtime.GOOS, runtime.GOARCH)
	fmt.Println("args:", os.Args[1:])
	if len(os.Args) < 2 {
		fmt.Fprintln(os.Stderr, "usage: exitdemo <isbn>")
		os.Exit(2)
	}
}
$ go run . a b
go1.27.2 darwin arm64
args: [a b]
$ echo $?
0

$ go run .
go1.27.2 darwin arm64
args: []
usage: exitdemo <isbn>
exit status 2
$ echo $?
1

$ go build -o exitdemo && ./exitdemo
go1.27.2 darwin arm64
args: []
usage: exitdemo <isbn>
$ echo $?
2

What is a module, and what happens without one

Outside a module the commands that need module information refuse to run, while go run file.go still compiles a standalone file:

$ go run main.go
no module here
$ go build
go: go.mod file not found in current directory or any parent directory; see 'go help modules'

Old tutorials describe a different world, the GOPATH workspace, where source lived under $GOPATH/src and there was no go.mod. That mode is still reachable through GO111MODULE=off (the build above succeeds with it in go1.27.2), but it is a legacy compatibility switch: new code always uses modules. See the end of this page for what changed since the books were written.

A module with several packages

Real programs are more than one file. The shelf module used throughout this section has this layout:

shelf/
    go.mod
    catalog/            package catalog: Book, Author, ISBN, Loan, LoanState, Store
        catalog.go
    internal/textutil/  helpers that only this module may import
        textutil.go
    cmd/shelf/          a command-line program
        main.go
    cmd/shelfd/         an HTTP service
        main.go

Conventions visible here: a package lives in one directory and its name is the last element of its import path; a cmd/ directory holds one subdirectory per executable; and an internal/ directory makes its packages importable only from within the parent of internal (enforced by the compiler). Exported names begin with a capital letter, and that is the entire visibility mechanism. The program in cmd/shelf/main.go uses the other two packages:

package main

import (
	"fmt"

	"example.com/shelf/catalog"
	"example.com/shelf/internal/textutil"
)

func main() {
	s := catalog.NewMemStore()
	isbn, _ := catalog.ParseISBN("978-0-13-419044-0")
	s.Put(catalog.Book{ISBN: isbn, Title: textutil.Normalize(" The Go Programming Language ")})
	fmt.Println(len(s.All()), catalog.OnLoan)
}
$ go list ./...
example.com/shelf/catalog
example.com/shelf/cmd/shelf
example.com/shelf/cmd/shelfd
example.com/shelf/internal/textutil

$ go build ./... && go vet ./... && go run ./cmd/shelf
1 on loan

$ gofmt -l .

./... is a package pattern: the current directory and every directory below it that contains Go files. go build ./... compiles everything but writes no executables when several packages are named; go install ./cmd/shelf installs one command. gofmt -l . lists files whose formatting differs and, printing nothing here, confirms every file is formatted. The packages themselves are covered in Packages, Imports and Initialization.

Calling a package from another module

The official create a module tutorial builds a library in one module and uses it from another. Here is the same idea, condensed, with a welcome library for the shelf front desk. Two sibling directories, each its own module:

module example.com/welcome

go 1.27
// Package welcome builds greeting messages for library members.
package welcome

import (
	"errors"
	"fmt"
)

// Message returns a greeting for the named member.
func Message(member string) (string, error) {
	if member == "" {
		return "", errors.New("welcome: empty member name")
	}
	return fmt.Sprintf("Welcome to the shelf, %s!", member), nil
}

The caller lives in desk/, a second module that imports example.com/welcome:

package main

import (
	"fmt"
	"log"

	"example.com/welcome"
)

func main() {
	log.SetFlags(0)
	log.SetPrefix("desk: ")

	msg, err := welcome.Message("Ada")
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(msg)

	if _, err := welcome.Message(""); err != nil {
		log.Fatal(err)
	}
}

Building straight away fails, because nothing tells the go command where example.com/welcome comes from. For a published module go get or go mod tidy would fetch it. For a module on your own disk, a replace directive points to the directory:

$ cd desk
$ go build
desk.go:7:2: no required module provides package example.com/welcome; to add it:
	go get example.com/welcome

$ go mod edit -replace example.com/welcome=../welcome
$ go mod tidy
go: found example.com/welcome in example.com/welcome v0.0.0-00010101000000-000000000000

$ cat go.mod
module example.com/desk

go 1.27

replace example.com/welcome => ../welcome

require example.com/welcome v0.0.0-00010101000000-000000000000

$ go run .
Welcome to the shelf, Ada!
desk: welcome: empty member name
exit status 1

The second call deliberately passes an empty name: log.Fatal prints the error with the desk: ` prefix and exits with status 1, which `go run reports as exit status 1. The placeholder version v0.0.0-00010101000000-000000000000 is what the go command invents for a directory replacement.

A replace is part of go.mod, so it travels with the repository, which is rarely what you want for a local-only arrangement. The alternative is a workspace: a go.work file next to both modules that use`s them, with no changes to either `go.mod:

go 1.27

use (
	./desk
	./welcome
)
$ go work init ./welcome ./desk
$ go work edit -go=1.27
$ cd desk && go run .
Welcome to the shelf, Ada!
desk: welcome: empty member name
exit status 1

Workspaces, go.work and publishing a module with proper version tags are the subject of Workspaces, Versioning and Publishing.

Testing the library

go test is the same tool again. A test is a function named TestXxx(t *testing.T) in a _test.go file of the same package:

package welcome

import "testing"

func TestMessage(t *testing.T) {
	got, err := Message("Ada")
	if err != nil {
		t.Fatalf("Message(%q) returned error: %v", "Ada", err)
	}
	if want := "Welcome to the shelf, Ada!"; got != want {
		t.Errorf("Message(%q) = %q, want %q", "Ada", got, want)
	}
}

func TestMessageEmpty(t *testing.T) {
	if _, err := Message(""); err == nil {
		t.Error(`Message("") returned no error`)
	}
}
$ go test -v
=== RUN   TestMessage
--- PASS: TestMessage (0.00s)
=== RUN   TestMessageEmpty
--- PASS: TestMessageEmpty (0.00s)
PASS
ok  	example.com/welcome	0.068s

$ go test -cover
PASS
coverage: 100.0% of statements
ok  	example.com/welcome	0.199s

(The elapsed times in the ok lines change from run to run.) Testing in depth is in Testing.

Reading documentation from the terminal

go doc shows the doc comments of any package or symbol, including your own and the standard library’s, without leaving the terminal:

$ go doc example.com/welcome
package welcome // import "example.com/welcome"

Package welcome builds greeting messages for library members.

func Message(member string) (string, error)

$ go doc welcome.Message
package welcome // import "example.com/welcome"

func Message(member string) (string, error)
    Message returns a greeting for the named member.

$ go doc -short fmt | head -4
func Append(b []byte, a ...any) []byte
func Appendf(b []byte, format string, a ...any) []byte
func Appendln(b []byte, a ...any) []byte
func Errorf(format string, a ...any) (err error)

Go 1.27 adds go doc package@version (documentation of a module version, without adding it to your module) and go doc -ex to list the executable examples of a package or symbol. The comment conventions, which go doc and pkg.go.dev both render, are in Documentation and Doc Comments.

Formatting: gofmt and the semicolon rule

Go has one canonical layout, produced by the gofmt tool. It indents with tabs, aligns comments and struct fields, and leaves line breaks where you put them. Because there is exactly one result, formatting arguments do not happen. go fmt runs gofmt -l -w over a package; gofmt -d shows a diff instead of rewriting; gofmt -s also simplifies code.

package main

import "fmt"

func main() {
        titles := []string{"Dune",
                "Emma"}
	for  _,t := range titles{
	fmt.Println(t)
	}
}

Source: cmd/gofmt

(This sample is deliberately badly formatted; every other example in this section is gofmt-clean.)

$ gofmt -l .
main.go
$ gofmt -d main.go
diff main.go.orig main.go
--- main.go.orig
+++ main.go
@@ -3,9 +3,9 @@
 import "fmt"

 func main() {
-        titles := []string{"Dune",
-                "Emma"}
-	for  _,t := range titles{
-	fmt.Println(t)
+	titles := []string{"Dune",
+		"Emma"}
+	for _, t := range titles {
+		fmt.Println(t)
 	}
 }
$ go fmt ./...
main.go

Formatting never changes meaning, with one exception that comes from the language itself: semicolon insertion. The grammar uses semicolons, but the lexer inserts one automatically at the end of a line that ends in an identifier, a literal, one of break, continue, fallthrough, return, one of the operators ++ or --, or a closing ), ] or }. The practical consequence is that the opening brace must stay on the line of the statement it opens:

package main

import "fmt"

func main()
{
	fmt.Println("hi")
}
$ go build
# example.com/fmtdemo
./main.go:6:1: syntax error: unexpected semicolon or newline before {

The line func main() ended in ), so a semicolon was inserted and the { on the next line no longer belongs to anything. The same rule is why a multi-line call or composite literal needs a trailing comma after its last element. The full lexical rules are in Program Structure and Lexical Elements.

How go build compiles a program

go build hides a long pipeline behind one command. The figure shows the stages from source to executable and where the neighbouring tools (gofmt, go vet and the build cache) fit.

Diagram of the go build pipeline. Stage 1, compile, once per package: Source (.go files, build tags, go.mod, embed files), Parse (tokens, semicolon insertion, syntax tree), Type-check (types, constants, unused imports and variables), Optimize (inlining, escape analysis, devirtualization), SSA and codegen (SSA passes, register allocation, assembler), then a Package archive (object code plus export data) stored in the build cache. gofmt rewrites the source layout, go vet type-checks and runs analyzers without generating code, and the build cache is keyed by a hash of the inputs. Stage 2, link, once per executable: package archives, the runtime (scheduler, garbage collector, allocator, compiled in), the linker (resolves symbols, drops unreachable functions, adds build info and debug data), and a single self-contained executable.

The compiler (go tool compile) works one package at a time, in dependency order. It parses source into a syntax tree (inserting semicolons as described above), type-checks it, optimizes it (inlining small functions, deciding which values may stay on the stack), turns it into an SSA form where most optimizations run, and emits machine code. The result for a package is an archive: object code plus the exported type information that importers need. The build cache stores each archive under a hash of everything that influenced it, so unchanged packages, including the standard library, are not recompiled. The linker (go tool link) then reads the archives of the program and all its dependencies together with the runtime package (scheduler, garbage collector, memory allocator), discards unreachable functions and writes one executable. Nothing else needs to be present on the machine that runs it.

You can watch the tool invocations. -n prints the commands without running them (-x prints and runs them); with -a to ignore the cache and a filter to keep only the compile of the program and the link, with the path to the toolchain shortened:

$ go build -a -n -o shelf . 2>&1 | grep -E 'tool/darwin_arm64/(compile|link) ' | grep -E ' -p (main|example.com/shelf) |/link '
$GOROOT/pkg/tool/darwin_arm64/compile -o $WORK/b001/_pkg_.a -trimpath "$WORK/b001=>" -p main -lang=go1.27 -complete -buildid ID -goversion go1.27.2 -c=10 -shared -nolocalimports -importcfg $WORK/b001/importcfg -pack ./main.go
$GOROOT/pkg/tool/darwin_arm64/link -o $WORK/b001/exe/a.out -importcfg $WORK/b001/importcfg.link -buildmode=pie -buildid=ID -extld=clang $WORK/b001/_pkg_.a

Two things stand out. The compiler is told -lang=go1.27, which comes straight from the go line of go.mod: that line chooses the language version a package is compiled with (see the toolchain page). And the link step takes only the archive of the program plus an import configuration naming every dependency’s archive.

Looking inside a build

The compiler can explain its decisions. This program has a function small enough to inline and a constructor whose result escapes to the heap:

package main

import "fmt"

type Book struct {
	Title string
	Pages int
}

// totalPages is small enough for the compiler to inline.
func totalPages(books []Book) int {
	n := 0
	for _, b := range books {
		n += b.Pages
	}
	return n
}

func newBook(title string, pages int) *Book {
	return &Book{Title: title, Pages: pages} // escapes: returned to the caller
}

func main() {
	books := []Book{*newBook("Dune", 412), *newBook("Emma", 474)}
	fmt.Println(totalPages(books))
}
$ go run .
886

$ go build -gcflags=-m
# example.com/shelf
./main.go:11:6: can inline totalPages
./main.go:19:6: can inline newBook
./main.go:24:26: inlining call to newBook
./main.go:24:49: inlining call to newBook
./main.go:25:24: inlining call to totalPages
./main.go:25:13: inlining call to fmt.Println
./main.go:11:17: books does not escape
./main.go:19:14: leaking param: title
./main.go:20:9: &Book{...} escapes to heap
./main.go:24:17: []Book{...} does not escape
./main.go:24:26: &Book{...} does not escape
./main.go:24:49: &Book{...} does not escape
./main.go:25:13: ... argument does not escape
./main.go:25:24: ~r0 escapes to heap

Source: cmd/compile

Notice that newBook is inlined into main, so after inlining its &Book{...} no longer escapes. -gcflags=-m is the first tool for understanding allocations; the full story, with -m -m, GC traces and benchmarks, is in Memory Management and Garbage Collection and Performance and PGO. GOSSAFUNC=totalPages go build writes an ssa.html file showing the function after each SSA pass; the build prints dumped SSA for totalPages,1 to ./ssa.html.

Every binary records how it was built. go version -m reads that information back, without needing the source:

$ go version ./shelf
./shelf: go1.27.2

$ go version -m ./shelf
./shelf: go1.27.2
	path	example.com/shelf
	mod	example.com/shelf	(devel)
	build	-buildmode=exe
	build	-compiler=gc
	build	CGO_ENABLED=1
	build	CGO_CFLAGS=
	build	CGO_CPPFLAGS=
	build	CGO_CXXFLAGS=
	build	CGO_LDFLAGS=
	build	GOARCH=arm64
	build	GOOS=darwin
	build	GOARM64=v8.0

Binaries are self-contained and easy to ship. A few linker and build flags change them; the sizes below are for this tiny program on darwin/arm64, where -s -w strips the symbol table and DWARF debug information:

$ go build -o shelf .
$ go build -ldflags='-s -w' -o shelf-small .
$ ls -l shelf shelf-small | awk '{print $5, $9}'
2429698 shelf
1587714 shelf-small
$ file shelf
shelf: Mach-O 64-bit executable arm64
Flag Effect

-o file-or-dir

name or directory of the output

-v

print the names of packages as they are compiled

-n, -x

print the commands (-n without executing them)

-work

keep the temporary work directory and print its name

-a

rebuild everything, ignoring the cache

-race

build with the data race detector (see Memory Model and Data Races)

-trimpath

remove file system paths from the binary, for reproducible builds

-ldflags 'flags'

flags for the linker, such as -s -w or -X main.version=1.2.3

-gcflags 'flags'

flags for the compiler, such as -m, -S or -N -l (disable optimizations, for debuggers)

-tags a,b

additional build tags (see The go Command, Builds and Cross-Compilation)

-mod=mod|readonly|vendor

how module files may be updated

-buildvcs

embed version-control information in the binary (default auto)

-json

machine-readable build output

-C dir

change to a directory before running

-p n

number of programs, such as compiles, that may run in parallel

Cross-compiling is a pair of variables

Because the toolchain is the same for every target, building for another operating system or architecture only needs GOOS and GOARCH, with no extra toolchain to install:

$ GOOS=linux GOARCH=amd64 go build -o shelf-linux .
$ file shelf-linux
shelf-linux: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, with debug_info, not stripped

$ GOOS=windows GOARCH=amd64 go build -o shelf.exe .
$ file shelf.exe
shelf.exe: PE32+ executable (console) x86-64, for MS Windows

$ go tool dist list | wc -l
      47

(The file output for the Linux binary also contains Go build and GNU build IDs, left out here.) go tool dist list prints every supported GOOS/GOARCH pair; cgo, which links C code, complicates cross-compiling and is covered in cgo and C Interoperability.

Experimenting without installing

The Go Playground at https://go.dev/play/ compiles and runs a single program on Google’s servers and gives you a link to share it. It is perfect for small experiments, bug reports and snippets in a chat. It runs in a sandbox: limited CPU and memory, no real network, a fake file system, and a fake clock that starts on 2009-11-10 and advances only when programs sleep, so that goroutine demos behave sensibly without waiting. Programs that need the network, real time or several files need a real toolchain.

A Tour of Go at https://go.dev/tour/ is an interactive introduction in four parts: basic syntax and data structures, methods and interfaces, generics, and concurrency, with exercises. It runs in the browser on the Playground, or locally with go install golang.org/x/website/tour@latest (network access required; not run here). Read it together with this section.

Reading compiler errors and vet reports

Compiler errors have the form file:line:column: message and come in the order of the lines they refer to. The compiler reports at most ten errors and then prints too many errors; -gcflags=-e removes that limit. One broken file shows several of the most common ones at once (the program is deliberately wrong and does not compile):

package main

import (
	"fmt"
	"os"
)

func title(isbn string) string {
	if isbn == "" {
		return "unknown"
	}
}

func main() {
	count := 3
	var name string = 42
	fmt.Println(undefinedHelper(name))
}
$ go build
# example.com/errs
./main.go:5:2: "os" imported and not used
./main.go:12:1: missing return
./main.go:15:2: declared and not used: count
./main.go:16:20: cannot use 42 (untyped int constant) as string value in variable declaration
./main.go:17:14: undefined: undefinedHelper
Message Cause Fix

"os" imported and not used

an import nothing refers to

delete the import (editors with gopls do it on save)

declared and not used: count

a local variable is declared but never read; Go treats it as an error, not a warning

use it, or assign to _

missing return

a function with results can reach its closing brace

return on every path, or end with panic

cannot use 42 (untyped int constant) as string value in variable declaration

an assignability error; Go has no implicit conversions between different types

convert explicitly or fix the type

undefined: undefinedHelper

a name that is not in scope or not exported from its package

check the spelling, the import and the capitalization

The ten-error limit in action, with twelve unused variables in one function:

$ go build
# example.com/errs
./many.go:4:2: declared and not used: a1
./many.go:5:2: declared and not used: a2
./many.go:6:2: declared and not used: a3
./many.go:7:2: declared and not used: a4
./many.go:8:2: declared and not used: a5
./many.go:9:2: declared and not used: a6
./many.go:10:2: declared and not used: a7
./many.go:11:2: declared and not used: a8
./many.go:12:2: declared and not used: a9
./many.go:13:2: declared and not used: a10
./many.go:13:2: too many errors
$ go build -gcflags=-e 2>&1 | tail -3
./many.go:13:2: declared and not used: a10
./many.go:14:2: declared and not used: a11
./many.go:15:2: declared and not used: a12

go vet: mistakes that compile

The compiler accepts any program that is well formed according to the spec. go vet goes further and flags code that is legal but almost certainly wrong. Here a format verb does not match its argument, a struct holding a mutex is copied, a variable is assigned to itself, and a message ends with a redundant newline:

package main

import (
	"fmt"
	"sync"
)

type Shelf struct {
	mu    sync.Mutex
	books []string
}

func report(s Shelf) int { return len(s.books) }

func main() {
	title := "Dune"
	count := 3
	fmt.Printf("%d copies of %s\n", title, count)
	fmt.Println("done\n")
	s := Shelf{books: []string{title}}
	fmt.Println(report(s))
	count = count
}
$ go build && echo build-ok
build-ok
$ go vet
main.go:22:2: self-assignment of count
main.go:13:15: report passes lock by value: example.com/vetdemo.Shelf contains sync.Mutex
main.go:21:21: call of report copies lock value: example.com/vetdemo.Shelf contains sync.Mutex
main.go:18:14: fmt.Printf format %d has arg title of wrong type string
main.go:19:2: fmt.Println arg list ends with redundant newline
$ echo $?
1
$ ./vetdemo
%!d(string=Dune) copies of %!s(int=3)
done

1

go vet ran analyzers in parallel here, so the order of the lines can differ between runs; the set of reports does not. The program builds and runs, printing the telltale %!d(string=Dune) instead of failing, which is exactly why vet matters.

If a file does not compile, vet cannot analyze it and reports the compile error as its own:

$ go vet
# example.com/errs
# [example.com/errs]
vet: ./main.go:12:1: missing return

go vet -json prints diagnostics (with suggested fixes where an analyzer has one) as JSON, and each analyzer has a flag to turn it on or off, as in go vet -printf=false. go test runs a high-confidence subset of vet automatically (atomic, bools, buildtag, directive, errorsas, ifaceassert, nilfunc, printf, stdversion, stringintconv and tests) before running the tests and refuses to run them if it finds a problem; -vet=off disables this and -vet=all runs everything. The analyzers that ship in go1.27.2 are:

Analyzer Reports

appends

append calls with no values to add

asmdecl

mismatches between assembly files and Go declarations

assign

useless assignments, such as x = x

atomic

common mistakes with sync/atomic

bools

redundant or suspect boolean expressions

buildtag

malformed //go:build and // +build lines

cgocall

violations of the cgo pointer-passing rules

composites

unkeyed fields in composite literals of imported struct types

copylocks

locks copied by value

defers

mistakes in defer statements

directive

misplaced Go toolchain directives such as //go:debug

errorsas

non-pointer second arguments to errors.As

framepointer

assembly that clobbers the frame pointer

hostport

malformed addresses passed to net.Dial

httpresponse

using an HTTP response before checking the error

ifaceassert

impossible interface-to-interface assertions

loopclosure

references to loop variables from nested functions (relevant only for modules older than go 1.22)

lostcancel

a context.CancelFunc that is never called

nilfunc

useless comparisons between functions and nil

printf

wrong format strings or arguments in Printf-style functions

shift

shifts that equal or exceed the width of the integer

sigchanyzer

unbuffered channels passed to signal.Notify

slog

invalid key-value argument lists for log/slog

stdmethods

wrong signatures of well-known methods such as String and Format

stdversion

use of standard-library symbols too new for the module’s go line

stringintconv

string(int) conversions

structtag

malformed struct tags

testinggoroutine

calls to t.Fatal from goroutines started by the test

tests

malformed test, benchmark, fuzz and example declarations

timeformat

the day-month swap in the layout 2006-02-01

unmarshal

passing non-pointers to Unmarshal functions

unreachable

unreachable code

unsafeptr

invalid conversions of uintptr to unsafe.Pointer

unusedresult

unused results of calls to functions such as fmt.Sprintf

waitgroup

misuse of sync.WaitGroup

The stdversion analyzer, run by go test by default since Go 1.27, is a good illustration of how the go line in go.mod controls what you may use: calling a function that is too new for the module is reported as a vet error and not as a compile error. The next page shows it in action. go vet and the wider static-analysis toolbox (go fix modernizers, gopls analyzers, staticcheck) are covered in Formatting, vet, fix and Static Analysis.

Common pitfalls when starting out

  • Working outside a module. go build and go test need a go.mod. Run go mod init <path> first; the error message tells you what to do.

  • Forgetting go mod tidy. After adding an import, go mod tidy adds the missing requirement and removes unused ones.

  • Expecting go run to preserve the exit status. It prints exit status N and returns 1.

  • Putting the opening brace on its own line. The inserted semicolon turns it into a syntax error.

  • Leaving unused variables and imports. They are compile errors, not warnings. Let the editor remove them.

  • Treating go vet output as optional. Printf-style mismatches and copied locks compile and fail at run time.

  • Installing tools into the module. Use go install tool@version; go get no longer installs binaries.

  • Editing GOPATH and GOROOT by hand. You almost never need to. GOROOT is inferred and GOPATH is only a cache and install location.

  • Setting GOOS and GOARCH with go env -w. That changes every build you make afterwards; set them per command as shown above.

  • Reading old tutorials. Anything that mentions $GOPATH/src, godoc, golint, io/ioutil or go get to build a program predates modules. The next section lists what changed.

Where the books are outdated

Both books used for this section were written before modules, the module proxy, the toolchain line and go fix modernizers existed in their current form; the facts they teach about the language are largely still right, but the set-up chapters are not.

Topic Book says Current approach

Workspace

Vivien ch. 1 and 6 and Bodner ch. 1 set up a GOPATH workspace and put code under $GOPATH/src

modules (introduced in 1.11, the default since 1.16): a go.mod anywhere on disk; GOPATH is only a cache and install location

Installing programs

Vivien ch. 1 and 6 use go get to download and build a program

go install pkg@version builds and installs; since Go 1.17 and 1.18 go get only edits go.mod and no longer builds or installs

Installing several versions

Bodner ch. 1 installs secondary toolchains with go get golang.org/dl/...

the go line and toolchain line in go.mod, with GOTOOLCHAIN=auto, fetch toolchains on demand since Go 1.21

Windows PATH

Bodner ch. 1 shows a manual command for editing PATH

the MSI installer sets PATH for you; if you do it by hand, add the directory that contains go.exe (\go\bin under the installation directory) and edit the variable through the system settings dialog

Lint tool

both books recommend golint

golint is archived; use go vet (which go test runs in part), the gopls analyzers and, from the community, staticcheck or golangci-lint

Documentation tool

Vivien ch. 1 documents godoc

the godoc binary is gone; use go doc and pkg.go.dev; go doc -http serves local docs (Go 1.25) and package@version works since Go 1.27

Editors

Vivien ch. 1 lists the editor plugins of 2016, and the books mention analysis tools such as oracle and guru

gopls is the single language server behind VS Code, GoLand, Vim, Emacs and others; Delve remains the debugger

Imports

Bodner ch. 1 treats goimports as a separate tool to install

gopls organizes imports on save; gofmt itself does not add or remove imports

Playground

Vivien ch. 1 presents the Playground as it worked in 2016

it now has a fake network stack and file system; a fake clock still starts on 2009-11-10

Build

Bodner ch. 1 drives builds with Makefiles and Vivien ch. 1 and 6 build inside a GOPATH

module-aware go build ./..., go run ./cmd/name, the build cache and go tool replace most of that; go generate runs code generation

Version cadence

Bodner ch. 1 describes the six-month cadence and the compatibility promise

both still hold; since Go 1.21 the toolchain line and GODEBUG give precise control over which toolchain and which behaviors a module uses (see the toolchain page)

The books also show tool output captured with Go 1.6 to 1.16; compiler messages have been reworded since (for example declared and not used: count, which now names the variable), so do not match them literally.