Overview
Go is a compiled, garbage-collected language with a single go command that handles everything a project needs: dependency management (modules), building, testing, benchmarking, fuzzing, static analysis (go vet), code rewrites (go fix) and cross-compilation. A build produces one self-contained executable, so a Go program ships as one file with no runtime to install on the target.
This skill covers the language toolchain itself. For HTTP frameworks use the gin, go-echo or go-fiber skills; for CLI command trees use cobra. The current stable release is Go 1.27 (August 2026); Go supports the two most recent major releases (1.27 and 1.26).
Instructions
Install Go
Package managers first:
brew install go # macOS
winget install GoLang.Go # Windows (PowerShell)
On Linux, download the archive from go.dev, check it against the SHA-256 listed on https://go.dev/dl/, then extract it to /usr/local. If an older Go already lives in /usr/local/go, remove that directory first: the docs warn that extracting over an old tree produces a broken install.
curl -LO https://go.dev/dl/go1.27.1.linux-amd64.tar.gz
sha256sum go1.27.1.linux-amd64.tar.gz
sudo tar -C /usr/local -xzf go1.27.1.linux-amd64.tar.gz
Add the toolchain and installed tools to PATH (in ~/.profile, ~/.bashrc or ~/.zshrc), open a new shell and verify:
export PATH=$PATH:/usr/local/go/bin
export PATH="$PATH:$(go env GOPATH)/bin"
go version
Start a module
A module is a directory tree with a go.mod at its root. The module path is normally the repository URL.
mkdir -p ratelimit/cmd/rlcheck && cd ratelimit
go mod init github.com/northwind-labs/ratelimit
go.mod records the module path and the minimum Go version (go 1.27.1). Library code sits at the root; each executable gets its own package main directory under cmd/ (here cmd/rlcheck/).
Build, run and format
go run ./cmd/rlcheck -burst 5 # compile and run in one step
go build -o bin/rlcheck ./cmd/rlcheck
gofmt -l . && gofmt -w . # list, then fix, unformatted files
go vet ./... # ./... = every package below here; flags printf misuse, copied locks
Manage dependencies
go get golang.org/x/sync@latest # add or upgrade one module
go get golang.org/x/sync@v0.23.0 # pin an exact version
go mod tidy # add missing, drop unused requirements
go list -m -u all # show modules with newer versions in [brackets]
go mod why golang.org/x/sync/errgroup # which import pulls this in
go.sum holds checksums for every downloaded module; commit it together with go.mod. For private repositories, set GOPRIVATE=github.com/northwind-labs/* so the go command fetches them directly instead of through the public proxy and checksum database.
Tool dependencies (Go 1.24+)
Pin developer tools in go.mod instead of installing them globally, so every contributor and CI job runs the same version:
go get -tool golang.org/x/vuln/cmd/govulncheck@latest
go tool govulncheck ./...
This adds a tool golang.org/x/vuln/cmd/govulncheck line to go.mod. To install a program on your machine instead, use go install golang.org/x/tools/gopls@latest; binaries land in $(go env GOPATH)/bin (or $GOBIN).
Test, benchmark, fuzz
Tests live in *_test.go files next to the code. A table-driven test with subtests:
func TestAllow(t *testing.T) {
tests := []struct {
name string
burst int
calls int
want int
}{
{"within burst", 5, 3, 3},
{"exceeds burst", 5, 8, 5},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := New(tt.burst, 1)
got := 0
for range tt.calls {
if l.Allow() {
got++
}
}
if got != tt.want {
t.Errorf("allowed %d, want %d", got, tt.want)
}
})
}
}
func BenchmarkAllow(b *testing.B) {
l := New(1_000_000, 1_000_000)
for b.Loop() {
l.Allow()
}
}
go test ./... # all packages; results are cached
go test -count=1 ./... # bypass the test cache
go test -run 'TestAllow/exceeds' -v . # one subtest (spaces become _)
go test -race ./... # data race detector
go test -coverprofile=cover.out ./... && go tool cover -func=cover.out
go test -bench=Allow -benchmem -run='^$' . # benchmarks only
go test -fuzz=FuzzMax -fuzztime=30s . # fuzz one target
A fuzz target starts with Fuzz, seeds inputs with f.Add and checks a property in f.Fuzz. Failing inputs are saved under testdata/fuzz/ and replayed by every later go test.
Generics
Type parameters work on functions and types; cmp.Ordered and comparable cover most constraints.
// imports: cmp, slices
func TopN[T cmp.Ordered](xs []T, n int) []T {
s := slices.Clone(xs)
slices.SortFunc(s, func(a, b T) int { return cmp.Compare(b, a) })
return s[:min(n, len(s))]
}
type Set[K comparable] map[K]struct{}
func (s Set[K]) Add(k K) { s[k] = struct{}{} }
func (s Set[K]) Has(k K) bool { _, ok := s[k]; return ok }
Go 1.27 also allows methods to declare their own type parameters (func (b Box) Scale[T int | float64](f T) T); such code needs go 1.27 in go.mod.
Multi-module workspaces
When a service and a library live in separate modules and you change both at once:
go work init ./gateway ./ratelimit
go work use ./billing # add another module later
go run ./gateway
go.work makes the local copies win over the versions in each go.mod, so there is no replace directive to forget. Usually keep it out of the repository.
Cross-compile release binaries
Set GOOS/GOARCH to build for another platform; go tool dist list shows every supported pair. CGO_ENABLED=0 gives a fully static binary with no libc dependency.
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -trimpath \
-ldflags "-s -w -X main.version=v1.4.0" -o dist/rlcheck-linux-arm64 ./cmd/rlcheck
-trimpathstrips local file paths from the binary.-ldflags "-s -w"drops symbol and debug tables (smaller file).-X main.version=v1.4.0sets a package-levelvar version stringat link time.go version -m dist/rlcheck-linux-arm64prints the Go version, module versions and build settings embedded in any Go binary.
Examples
Example 1: Ship a CLI to four platforms
User request: "Build our rlcheck tool for Linux (x86 and ARM), Apple Silicon Macs and Windows, stamped with version v1.4.0."
for target in linux/amd64 linux/arm64 darwin/arm64 windows/amd64; do
os=${target%/*}; arch=${target#*/}; ext=""
[ "$os" = windows ] && ext=.exe
CGO_ENABLED=0 GOOS=$os GOARCH=$arch go build -trimpath \
-ldflags "-s -w -X main.version=v1.4.0" \
-o "dist/rlcheck-$os-$arch$ext" ./cmd/rlcheck
done
file dist/*
./dist/rlcheck-linux-amd64 -version
Result: four files of about 1.6 MB each. file reports ELF 64-bit ... statically linked, stripped for the Linux builds, Mach-O 64-bit arm64 executable for macOS and PE32+ executable (console) x86-64 for Windows. The Linux binary prints rlcheck v1.4.0. Nothing else has to be installed on the target machines.
Example 2: Add a dependency, test for races and scan for vulnerabilities
User request: "Our health checker should probe hosts in parallel, at most 4 at a time. Add that, make sure there are no races, and check dependencies for known CVEs before we merge."
package main
import (
"context"
"golang.org/x/sync/errgroup"
)
func checkAll(ctx context.Context, hosts []string, check func(context.Context, string) error) error {
g, ctx := errgroup.WithContext(ctx)
g.SetLimit(4)
for _, h := range hosts {
g.Go(func() error { return check(ctx, h) })
}
return g.Wait()
}
go get golang.org/x/sync@latest
go mod tidy
go vet ./...
go test -race -count=1 ./...
go get -tool golang.org/x/vuln/cmd/govulncheck@latest
go tool govulncheck ./...
Result: go.mod gains require golang.org/x/sync v0.23.0 and a tool line for govulncheck. go test -race prints ok for each package, or a WARNING: DATA RACE report with both goroutines' stacks. govulncheck ends with No vulnerabilities found. or lists each vulnerable call it actually reaches, with the fixed version.
Example 3: Small image for a Go API in Docker
User request: "Our Go gateway image is almost 1 GB because it ships the whole toolchain. Make it small."
FROM golang:1.27-alpine AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" -o /out/gateway ./cmd/gateway
FROM gcr.io/distroless/static-debian13:nonroot
COPY --from=build /out/gateway /gateway
ENTRYPOINT ["/gateway"]
Result: go mod download sits in its own layer, so it is cached until go.mod/go.sum change. The final image contains only the static binary on a distroless base and runs as a non-root user, a small fraction of the size of the full golang image.
Guidelines
- Run
gofmt,go vet ./...andgo test -race ./...in CI. The race detector only finds races on code paths the tests actually execute; it requires cgo (CGO_ENABLED=1) and a supported target such as linux/amd64, darwin/arm64 or windows/amd64. go testcaches passing results for unchanged code (it does notice changed files in the module and changed environment variables). Use-count=1when a test depends on external state such as a database or network service.- Commit
go.modandgo.sumtogether, and rungo mod tidybefore committing. Do not editgo.sumby hand. go install pkg@versionignores the current module'sgo.mod; use it for machine-wide tools. Usego get -toolfor tools the project depends on.CGO_ENABLED=0fails for packages that need C (some SQLite drivers, for example). Pick a pure-Go alternative or cross-compile with a C toolchain for the target.-Xonly sets string variables, and needs the full import path for non-main packages (-X github.com/northwind-labs/ratelimit/internal/build.Version=v1.4.0).- Private module credentials belong in git's credential helper or
~/.netrc, never ingo.modor the source tree. - The
goline ingo.modis the minimum Go version; raise it withgo get go@1.27.1. With the defaultGOTOOLCHAIN=autoan older go command downloads the required toolchain; setGOTOOLCHAIN=localin CI to fail instead. go fix ./...rewrites code to newer idioms; review the diff first withgo fix -diff ./....- When not to use Go: hard-real-time or GC-sensitive hot paths (look at Rust), quick data analysis (Python), or browser UI code.