Go (Golang) Cheat Sheet

This Go cheat sheet covers 30 sections: syntax, types and conversions, slices and maps, structs, interfaces, generics including the generic methods of Go 1.27, error handling, iterators, goroutines and channels, context, JSON, net/http, time layouts, modules, testing, profiling and the compiler errors you will actually meet. Search it, filter it by level or by Go version, copy any line with one click, and lint Go code, check Printf verbs or turn JSON into structs without leaving the page.

30sections
216snippets
Go 1.27latest release
7Go tools

Updated September 29, 2026, checked against Go 1.27.1 and the Go 1.25, 1.26 and 1.27 release notes. Print it for a PDF copy.

/
level

return nil

Nothing matches that search. Try a shorter keyword such as slice, goroutine or json. Several words are combined, so every one of them has to match. Keys: / focuses the search, Esc clears it, t switches the theme.

01

Install, Run & Build

installing Go, go run and go build, toolchains, a Go toolbox and builders

Go Toolbox: Lint, Printf Verbs, JSON to Struct, Struct Layout, Time Layouts

core
Ctrl+Enter lints. Check Printf verbs matches every verb with its argument, like go vet. JSON to Go struct writes types with json tags. Time layout turns %Y-%m-%d, yyyy-MM-dd or an example date such as Tue, 29 Sep 2026 14:05:09 GMT into a Go layout and explains any Go layout. Regex for Go flags what RE2 cannot do and writes the regexp.MustCompile call. Run on godbolt opens the program in Compiler Explorer in a new tab; nothing is sent until you press it.
output

          
Paste Go code, a Printf call, JSON, a struct, a date format, a regex or a go.mod above, then pick an action.

Builders: go build, Dockerfile, go test, GitHub Actions, Profiling

everyday
command

          

Install Go

core

macOS with Homebrew, or the official tarball on Linux

brew install go

curl -LO https://go.dev/dl/go1.27.1.linux-amd64.tar.gz
sudo rm -rf /usr/local/go
sudo tar -C /usr/local -xzf go1.27.1.linux-amd64.tar.gz
export PATH=$PATH:/usr/local/go/bin:$HOME/go/bin
go version

Windows with winget, or the MSI installer from go.dev/dl

winget install GoLang.Go
go version
go env GOPATH

No install at all: run the official image

docker run --rm -v "$PWD":/src -w /src golang:1.27 go run .
docker run --rm -v "$PWD":/src -w /src golang:1.27 go test ./...

Hello, World

core

A module, a main package and a main function

package main

import "fmt"

func main() {
    fmt.Println("Hello, Gopher")
}

The commands around it

mkdir hello && cd hello
go mod init example.com/hello
go run .
go build -o hello .
./hello

go run, build, install

everyday

The everyday commands

go run .                      # compile and run the package in this directory
go run ./cmd/server -port 8080  # arguments after the package go to the program
go build ./...                # compile every package, write nothing but the main binaries
go build -o bin/app ./cmd/app # choose the output file
go install ./cmd/app          # build and copy to $(go env GOPATH)/bin
go install golang.org/x/tools/gopls@latest   # install a tool from a module
go vet ./...                  # static checks the compiler skips
go fmt ./...                  # format every file (gofmt -l -w)

go env: where things live

go env GOPATH GOROOT GOMODCACHE GOCACHE
go env GOOS GOARCH CGO_ENABLED
go env -w GOPRIVATE=github.com/acme/*
go env -u GOPRIVATE

Toolchains

advanced

The go line picks the toolchain

go version                      # the toolchain you are running
go get go@1.27.1                # require at least Go 1.27.1 for this module
go get toolchain@go1.27.1       # prefer this toolchain for this module
GOTOOLCHAIN=local go build ./...  # never download, use the installed Go
GOTOOLCHAIN=go1.26.8 go version   # run a specific release once

A second Go version side by side

go install golang.org/dl/go1.26.8@latest
go1.26.8 download
go1.26.8 test ./...
02

Packages & Imports

package clauses, imports, exported names, init, internal packages and layout

Packages and Imports

core

One package per directory

package inventory

import (
    "fmt"
    "strings"

    "example.com/shop/internal/money"
    "github.com/google/go-cmp/cmp"
)

Alias, blank and dot imports

import (
    mrand "math/rand/v2"            // alias: rename to avoid a clash
    _ "github.com/lib/pq"           // blank: run the package's init only
    _ "embed"                       // needed for //go:embed
    . "github.com/onsi/gomega"      // dot: use names without a prefix, tests only
)

Capital letter means exported

package money

type Amount struct {
    Cents    int64  // exported: other packages can use it
    currency string // unexported: only package money sees it
}

func New(cents int64) Amount { return Amount{Cents: cents, currency: "USD"} }
func round(x float64) int64   { return int64(x + 0.5) }

init and Program Start

everyday

Package variables, then init, then main

package main

import "fmt"

var ready = setup()

func setup() bool {
    fmt.Println("1. package variables")
    return true
}

func init() {
    fmt.Println("2. init, ready =", ready)
}

func main() {
    fmt.Println("3. main")
}

Project Layout

everyday

A common layout for an application module

shop/
    go.mod                 module example.com/shop
    cmd/
        api/main.go        package main: one directory per binary
        worker/main.go
    internal/
        orders/orders.go   importable only inside example.com/shop
        money/money.go
    web/static/            files embedded with //go:embed

Doc comments and go doc

// Package money handles amounts in integer cents.
package money

// Add returns the sum of a and b. It panics if the currencies differ.
func Add(a, b Amount) Amount { return Amount{Cents: a.Cents + b.Cents, currency: a.currency} }

go doc example.com/shop/internal/money Add
go doc -all strings
go doc net/http@v1.27.0 Client

Built-in Functions

core

The functions every package can use without an import

s := make([]int, 2, 5)
s = append(s, 7)
dst := make([]int, 2)
n := copy(dst, s)
m := map[string]int{"a": 1, "b": 2}
delete(m, "a")
fmt.Println(len(s), cap(s), n, dst, len(m))
fmt.Println(min(3, 1, 2), max(2.5, 1), min("pear", "apple"))
c := complex(1, 2)
fmt.Println(real(c), imag(c))
clear(m)
fmt.Println(len(m), len("héllo"))
03

Variables & Constants

var, short declarations, zero values, shadowing, constants and iota

Declaring Variables

core

var, :=, and several at once

var name string = "gopher"
var count int
age := 14
x, y := 1, 2
x, y = y, x
var (
    host = "localhost"
    port = 8080
)
fmt.Println(name, count, age, x, y, host, port)

Zero values: nothing is ever uninitialized

var i int
var f float64
var b bool
var s string
var p *int
var sl []int
var m map[string]int
var err error
fmt.Printf("%v %v %v %q %v %v %v %v\n", i, f, b, s, p, sl, m, err)
fmt.Println(sl == nil, m == nil, len(sl), len(m))

Shadowing with := in an inner scope

x := 1
if true {
    x := 2
    x++
    fmt.Println("inner", x)
}
fmt.Println("outer", x)

The blank identifier

_, err := strconv.Atoi("42")
for _, v := range []string{"a", "b"} {
    fmt.Print(v)
}
var _ fmt.Stringer = (*strings.Builder)(nil)
fmt.Println("", err)

Constants and iota

core

iota numbers constants in a block

package main

import "fmt"

type Weekday int

const (
    Sunday Weekday = iota
    Monday
    Tuesday
)

const (
    _  = iota
    KB = 1 << (10 * iota)
    MB
    GB
)

type Perm uint8

const (
    Read Perm = 1 << iota
    Write
    Exec
)

func main() {
    fmt.Println(Sunday, Monday, Tuesday)
    fmt.Println(KB, MB, GB)
    fmt.Printf("%03b %03b %03b %03b\n", Read, Write, Exec, Read|Exec)
}

Untyped constants have arbitrary precision

const huge = 1 << 100
const small = huge >> 98
var f float64 = huge
fmt.Println(small, f)
const c = 'A' + 1
fmt.Printf("%c %T\n", c, c)

new with a Value

Go 1.26+advanced

new(expr) allocates and initializes in one step

package main

import "fmt"

type Profile struct {
    Name string
    Age  *int
}

func main() {
    p := Profile{Name: "ann", Age: new(36)}
    q := new("hello")
    fmt.Println(*p.Age, *q)
}
04

Types & Conversions

numeric types, conversions, strconv, overflow, type definitions and aliases

Basic Types

core

The built-in types

TypeSizeNotes
bool1 bytetrue or false
int, uint8 bytes on 64-bitthe default integer type; 4 bytes on 32-bit targets
int8 to int641 to 8 bytessigned, exact width
uint8 to uint641 to 8 bytesunsigned, exact width
byte1 bytealias for uint8
rune4 bytesalias for int32, one Unicode code point
uintptrpointer sizeinteger that can hold an address, for unsafe code
float32, float644, 8 bytesIEEE 754; float64 is the default
complex64, complex1288, 16 bytescomplex numbers
string16 bytes headerimmutable bytes, usually UTF-8

Sizes and limits

fmt.Println(math.MaxInt64, math.MinInt64)
fmt.Println(math.MaxUint32, math.MaxInt8)
fmt.Println(math.MaxFloat64, math.SmallestNonzeroFloat64)
fmt.Println(unsafe.Sizeof(int(0)), unsafe.Sizeof(""), unsafe.Sizeof([]int{}))

Conversions

core

Conversions are always explicit

i := 42
f := float64(i) / 5
u := uint8(i)
back := int(f)
fmt.Println(f, u, back)
big := 300
fmt.Println(uint8(big), int8(big))
neg, pos := -3.9, 3.9
fmt.Println(int(neg), int(pos))

Strings and numbers with strconv

n, err := strconv.Atoi("123")
fmt.Println(n, err)
_, err = strconv.Atoi("12a")
fmt.Println(err)
x, _ := strconv.ParseInt("ff", 16, 64)
fl, _ := strconv.ParseFloat("3.25", 64)
b, _ := strconv.ParseBool("true")
fmt.Println(x, fl, b)
fmt.Println(strconv.Itoa(99), strconv.FormatInt(255, 2), strconv.Quote("tab\there"))

Integer overflow wraps, division truncates

var b uint8 = 255
b++
var i int8 = 127
i++
fmt.Println(b, i)
fmt.Println(7/2, -7/2, 7%3, -7%3)
fmt.Println(7.0/2, math.Floor(-3.5), math.Round(2.5))

Integer division by zero panics

zero := 0
fmt.Println(10 / zero)

Defined Types and Aliases

everyday

A new type, or another name for one

package main

import "fmt"

type Celsius float64
type Fahrenheit float64

func (c Celsius) ToF() Fahrenheit { return Fahrenheit(c*9/5 + 32) }

type Temp = Celsius

func main() {
    var t Temp = 21.5
    fmt.Println(t.ToF())
    fmt.Printf("%T\n", t)
}

Generic type aliases

type Set[T comparable] = map[T]struct{}
type List[T any] = []T
05

Control Flow

if, for, range, switch, labels and goto

if and switch

core

if with a short statement

if n, err := strconv.Atoi("42"); err != nil {
    fmt.Println("bad number:", err)
} else if n > 40 {
    fmt.Println("big", n)
} else {
    fmt.Println("small", n)
}

switch: no fallthrough by default

day := "sat"
switch day {
case "sat", "sun":
    fmt.Println("weekend")
case "fri":
    fmt.Println("almost")
    fallthrough
default:
    fmt.Println("weekday")
}
x := 7
switch {
case x > 5:
    fmt.Println("more than 5")
case x > 0:
    fmt.Println("positive")
}

for Loops

core

The only loop keyword

for i := 0; i < 3; i++ {
    fmt.Print(i, " ")
}
n := 1
for n < 100 {
    n *= 3
}
fmt.Println(n)
for {
    n--
    if n%10 == 0 {
        break
    }
}
fmt.Println(n)

range over ints, slices, maps and strings

for i := range 3 {
    fmt.Print(i, " ")
}
fmt.Println()
for i, v := range []string{"a", "b"} {
    fmt.Print(i, "=", v, " ")
}
fmt.Println()
for i, r := range "héllo" {
    fmt.Print(i, ":", string(r), " ")
}
fmt.Println()

Each iteration has its own variable

var prints []func()
for i := range 3 {
    prints = append(prints, func() { fmt.Print(i, " ") })
}
for _, p := range prints {
    p()
}
fmt.Println()

Labels, break and continue

everyday

Break out of an outer loop

grid := [][]int{{1, 2}, {3, -1}, {5, 6}}
outer:
for r, row := range grid {
    for c, v := range row {
        if v < 0 {
            fmt.Println("negative at", r, c)
            break outer
        }
    }
}
for i := range 5 {
    if i%2 == 0 {
        continue
    }
    fmt.Print(i, " ")
}
fmt.Println()
06

Strings, Runes & Bytes

UTF-8 strings, the strings package, runes, bytes and building strings

String Basics

core

Strings are read-only bytes, usually UTF-8

s := "héllo, 世界"
fmt.Println(len(s), utf8.RuneCountInString(s))
fmt.Println(s[0], string(s[0]), s[1:3])
raw := `C:\path\no\escapes
two lines`
fmt.Println(raw)

Runes and bytes

s := "Go语言"
for i, r := range s {
    fmt.Printf("%d:%c(%U) ", i, r, r)
}
fmt.Println()
rs := []rune(s)
bs := []byte(s)
fmt.Println(len(rs), len(bs), string(rs[2:]))

Reverse a string by runes

func reverse(s string) string {
    r := []rune(s)
    for i, j := 0, len(r)-1; i < j; i, j = i+1, j-1 {
        r[i], r[j] = r[j], r[i]
    }
    return string(r)
}

func main() {
    fmt.Println(reverse("Hello, 世界"))
}

The strings Package

core

Search and test

s := "Go is expressive, concise, clean"
fmt.Println(strings.Contains(s, "concise"), strings.HasPrefix(s, "Go"), strings.HasSuffix(s, "!"))
fmt.Println(strings.Index(s, "is"), strings.LastIndex(s, "c"), strings.Count(s, "c"))
fmt.Println(strings.EqualFold("Gopher", "GOPHER"), strings.ContainsAny(s, "xyz"))

Split, join, trim and replace

fmt.Printf("%q\n", strings.Split("a,b,,c", ","))
fmt.Printf("%q\n", strings.Fields("  a  b \t c\n"))
fmt.Println(strings.Join([]string{"x", "y", "z"}, "-"))
fmt.Printf("%q\n", strings.TrimSpace("  padded \n"))
fmt.Println(strings.Trim("xxhixx", "x"), strings.TrimPrefix("v1.2", "v"), strings.TrimSuffix("a.go", ".go"))
fmt.Println(strings.ReplaceAll("a-b-c", "-", "+"), strings.Replace("a-b-c", "-", "+", 1))
fmt.Println(strings.ToUpper("go"), strings.Repeat("ab", 3), strings.ToLower("MiXeD"))

Cut, CutPrefix and CutLast

key, val, ok := strings.Cut("name=gopher=blue", "=")
fmt.Println(key, val, ok)
rest, found := strings.CutPrefix("Bearer abc123", "Bearer ")
fmt.Println(rest, found)
dir, file, _ := strings.CutLast("src/app/main.go", "/")
fmt.Println(dir, file)

Iterate lines and fields without building a slice

text := "alpha\nbeta\ngamma"
for line := range strings.Lines(text) {
    fmt.Printf("%q ", line)
}
fmt.Println()
for part := range strings.SplitSeq("a,b,c", ",") {
    fmt.Print(part, " ")
}
fmt.Println()

Building Strings

core

strings.Builder instead of += in a loop

var b strings.Builder
for i := range 3 {
    fmt.Fprintf(&b, "item%d;", i)
}
b.WriteString("done")
fmt.Println(b.String(), b.Len())

bytes: the same API for []byte

data := []byte("  Hello, Bytes  ")
data = bytes.TrimSpace(data)
fmt.Println(string(bytes.ToUpper(data)), bytes.Contains(data, []byte("Byte")))
var buf bytes.Buffer
buf.WriteString("line 1\n")
buf.WriteByte('x')
fmt.Printf("%q\n", buf.String())

Characters with unicode

for _, r := range "Go 1.27!" {
    switch {
    case unicode.IsUpper(r):
        fmt.Print("U")
    case unicode.IsLetter(r):
        fmt.Print("l")
    case unicode.IsDigit(r):
        fmt.Print("d")
    case unicode.IsSpace(r):
        fmt.Print("_")
    default:
        fmt.Print(".")
    }
}
fmt.Println()
07

Arrays & Slices

arrays, slices, append, copy, sharing, and the slices package

Arrays and Slices

core

Arrays are values, slices are views

a := [3]int{1, 2, 3}
b := a
b[0] = 99
fmt.Println(a, b, len(a))
s := []int{1, 2, 3}
t := s
t[0] = 99
fmt.Println(s, t)
auto := [...]string{"x", "y"}
fmt.Println(len(auto))

make, append, len and cap

s := make([]int, 0, 2)
for i := range 5 {
    s = append(s, i)
    fmt.Println(len(s), cap(s), s)
}
s = append(s, []int{7, 8}...)
fmt.Println(s)

Slicing shares the backing array

nums := []int{0, 1, 2, 3, 4, 5}
mid := nums[2:4]
mid[0] = 20
fmt.Println(nums, mid, len(mid), cap(mid))
mid = append(mid, 30)
fmt.Println(nums)
safe := nums[2:4:4]
safe = append(safe, 40)
fmt.Println(nums, safe)

copy, clone, and 2D slices

src := []int{1, 2, 3}
dst := make([]int, 2)
n := copy(dst, src)
fmt.Println(n, dst)
c := slices.Clone(src)
c[0] = 100
fmt.Println(src, c)
grid := make([][]rune, 2)
for i := range grid {
    grid[i] = []rune("..")
}
grid[1][0] = '#'
for _, row := range grid {
    fmt.Println(string(row))
}

The slices Package

Go 1.21+core

Sort, search and compare

s := []int{5, 2, 8, 2, 9}
slices.Sort(s)
fmt.Println(s, slices.Contains(s, 8), slices.Index(s, 8))
i, found := slices.BinarySearch(s, 5)
fmt.Println(i, found, slices.Max(s), slices.Min(s))
fmt.Println(slices.Compact(s), slices.Equal([]int{1}, []int{1}))

Sort structs by a field

package main

import (
    "cmp"
    "fmt"
    "slices"
)

type User struct {
    Name string
    Age  int
}

func main() {
    users := []User{{"ann", 31}, {"bob", 25}, {"cid", 31}}
    slices.SortStableFunc(users, func(a, b User) int {
        return cmp.Or(cmp.Compare(b.Age, a.Age), cmp.Compare(a.Name, b.Name))
    })
    fmt.Println(users)
    idx := slices.IndexFunc(users, func(u User) bool { return u.Name == "bob" })
    fmt.Println(idx)
}

Insert, delete and filter in place

s := []string{"a", "b", "c", "d"}
s = slices.Insert(s, 1, "x")
s = slices.Delete(s, 3, 4)
fmt.Println(s)
nums := []int{1, 2, 3, 4, 5, 6}
nums = slices.DeleteFunc(nums, func(n int) bool { return n%2 == 0 })
slices.Reverse(nums)
fmt.Println(nums, len(nums))

Chunks and repeats

for c := range slices.Chunk([]int{1, 2, 3, 4, 5}, 2) {
    fmt.Print(c, " ")
}
fmt.Println(slices.Repeat([]string{"ab"}, 3))
08

Maps

creating, reading, deleting, iterating in order, sets and the maps package

Map Basics

core

Create, read, write, delete

ages := map[string]int{"ann": 31, "bob": 25}
ages["cid"] = 40
fmt.Println(ages["ann"], ages["zoe"], len(ages))
age, ok := ages["zoe"]
fmt.Println(age, ok)
delete(ages, "bob")
if v, ok := ages["cid"]; ok {
    fmt.Println("cid is", v)
}
fmt.Println(ages)

Writing to a nil map panics

var m map[string]int
fmt.Println(m["x"], len(m))
m["x"] = 1

Iterate in a stable order

stock := map[string]int{"pear": 3, "apple": 5, "fig": 1}
for _, k := range slices.Sorted(maps.Keys(stock)) {
    fmt.Println(k, stock[k])
}

Common Map Patterns

everyday

Counting, grouping and sets

words := strings.Fields("the cat and the hat and the bat")
count := map[string]int{}
for _, w := range words {
    count[w]++
}
byLen := map[int][]string{}
for w := range count {
    byLen[len(w)] = append(byLen[len(w)], w)
}
seen := map[string]struct{}{}
seen["the"] = struct{}{}
_, dup := seen["the"]
fmt.Println(count["the"], len(byLen[3]), dup)

Map values are not addressable

package main

type Point struct{ X, Y int }

func main() {
    m := map[string]Point{"a": {1, 2}}
    m["a"].X = 10
}

The maps package and clear

a := map[string]int{"x": 1, "y": 2}
b := maps.Clone(a)
maps.Copy(b, map[string]int{"z": 3})
maps.DeleteFunc(b, func(k string, v int) bool { return v == 1 })
fmt.Println(a, b, maps.Equal(a, b))
clear(a)
fmt.Println(a, len(a))

Maps are not safe for concurrent writes

m := map[int]int{}
var wg sync.WaitGroup
for i := range 100 {
    wg.Go(func() { m[i] = i })
}
wg.Wait()
09

Structs & Embedding

struct types, literals, comparison, embedding, tags and constructors

Structs

core

Define, create and use

package main

import "fmt"

type User struct {
    ID     int
    Name   string
    Emails []string
    Admin  bool
}

func main() {
    u := User{ID: 1, Name: "ann"}
    u.Emails = append(u.Emails, "ann@example.com")
    p := &u
    p.Admin = true
    fmt.Printf("%v\n%+v\n", u, u)
    point := struct{ X, Y int }{3, 4}
    fmt.Println(point.X, point == struct{ X, Y int }{3, 4})
}

Constructors and functional options

package main

import (
    "fmt"
    "time"
)

type Server struct {
    addr    string
    timeout time.Duration
}

type Option func(*Server)

func WithTimeout(d time.Duration) Option { return func(s *Server) { s.timeout = d } }

func NewServer(addr string, opts ...Option) *Server {
    s := &Server{addr: addr, timeout: 30 * time.Second}
    for _, o := range opts {
        o(s)
    }
    return s
}

func main() {
    s := NewServer(":8080", WithTimeout(5*time.Second))
    fmt.Println(s.addr, s.timeout)
}

Embedding

everyday

Composition with promoted fields and methods

package main

import "fmt"

type Person struct{ Name string }

func (p Person) Hello() string { return "hi, I am " + p.Name }

type Employee struct {
    Person
    Company string
}

func main() {
    e := Employee{Person: Person{Name: "ann"}, Company: "acme"}
    fmt.Println(e.Name, e.Hello(), e.Person.Name)
}

Promoted field names in struct literals

package main

import "fmt"

type Point struct{ X, Y int }

type Pin struct {
    Point
    Label string
}

func main() {
    p := Pin{X: 1, Y: 2, Label: "home"}
    fmt.Printf("%+v\n", p)
}

Tags and Layout

advanced

Read struct tags with reflect

package main

import (
    "fmt"
    "reflect"
)

type Item struct {
    SKU   string  `json:"sku" db:"sku_code"`
    Price float64 `json:"price,omitzero"`
}

func main() {
    t := reflect.TypeFor[Item]()
    for f := range t.Fields() {
        fmt.Println(f.Name, f.Tag.Get("json"), f.Tag.Get("db"))
    }
}

Field order changes the size

package main

import (
    "fmt"
    "unsafe"
)

type Loose struct {
    a bool
    b int64
    c bool
}

type Tight struct {
    b int64
    a bool
    c bool
}

func main() {
    fmt.Println(unsafe.Sizeof(Loose{}), unsafe.Sizeof(Tight{}), unsafe.Sizeof(struct{}{}))
}
10

Pointers & Memory

pointers, value and pointer semantics, nil, escape analysis and the garbage collector

Pointers

core

& takes an address, * follows it

x := 10
p := &x
*p = 20
fmt.Println(x, *p, p != nil)
var q *int
fmt.Println(q == nil)

Everything is passed by value

package main

import "fmt"

type Counter struct{ N int }

func incValue(c Counter)    { c.N++ }
func incPointer(c *Counter) { c.N++ }
func addItem(s []int)       { s[0] = 99 }

func main() {
    c := Counter{}
    incValue(c)
    incPointer(&c)
    s := []int{1, 2}
    addItem(s)
    fmt.Println(c.N, s)
}

nil pointer dereference

package main

import "fmt"

type Node struct{ Val int }

func main() {
    var n *Node
    fmt.Println(n == nil)
    fmt.Println(n.Val)
}

Memory and the Garbage Collector

advanced

See what escapes to the heap

go build -gcflags=-m ./... 2>&1 | grep escape

Tune the collector

GOGC=200 ./app              # collect less often, use more memory
GOMEMLIMIT=1GiB ./app       # soft memory limit, useful in containers
GODEBUG=gctrace=1 ./app     # one line per collection
debug.SetMemoryLimit(1 << 30)

Weak pointers and cleanups

package main

import (
    "fmt"
    "runtime"
    "time"
    "weak"
)

type Blob struct{ data []byte }

func main() {
    done := make(chan int, 1)
    b := &Blob{data: make([]byte, 1<<20)}
    w := weak.Make(b)
    runtime.AddCleanup(b, func(n int) { done <- n }, len(b.data))
    fmt.Println("alive:", w.Value() != nil)
    b = nil
    runtime.GC()
    select {
    case n := <-done:
        fmt.Println("cleaned up", n, "bytes, weak pointer nil:", w.Value() == nil)
    case <-time.After(time.Second):
        fmt.Println("no cleanup yet")
    }
}
11

Functions & Closures

multiple returns, variadic functions, closures, defer and function values

Functions

core

Multiple results and named results

package main

import (
    "errors"
    "fmt"
)

func divmod(a, b int) (q, r int, err error) {
    if b == 0 {
        return 0, 0, errors.New("division by zero")
    }
    q, r = a/b, a%b
    return
}

func main() {
    q, r, err := divmod(17, 5)
    fmt.Println(q, r, err)
    _, _, err = divmod(1, 0)
    fmt.Println(err)
}

Variadic parameters

func sum(label string, nums ...int) string {
    total := 0
    for _, n := range nums {
        total += n
    }
    return fmt.Sprintf("%s=%d (%d args)", label, total, len(nums))
}

func main() {
    fmt.Println(sum("none"))
    fmt.Println(sum("three", 1, 2, 3))
    xs := []int{4, 5}
    fmt.Println(sum("slice", xs...))
}

Closures

core

A function that remembers its variables

func counter() func() int {
    n := 0
    return func() int {
        n++
        return n
    }
}

func main() {
    next := counter()
    fmt.Println(next(), next(), next())
    other := counter()
    fmt.Println(other())
}

Function types and method values

package main

import (
    "fmt"
    "strings"
)

type Transform func(string) string

func apply(s string, fs ...Transform) string {
    for _, f := range fs {
        s = f(s)
    }
    return s
}

func main() {
    r := strings.NewReplacer("a", "4", "e", "3")
    fmt.Println(apply("  leet speak ", strings.TrimSpace, strings.ToUpper))
    fmt.Println(apply("leet speak", r.Replace))
}

defer

core

Runs at return, last in first out

func main() {
    for i := range 3 {
        defer fmt.Println("deferred", i)
    }
    x := 10
    defer fmt.Println("x was", x)
    x = 20
    fmt.Println("body done, x =", x)
}

Clean up right after acquiring

f, err := os.Open(path)
if err != nil {
    return err
}
defer f.Close()

mu.Lock()
defer mu.Unlock()

defer can change named results

package main

import (
    "errors"
    "fmt"
)

func closeFile() error { return errors.New("close failed: disk full") }

func save() (err error) {
    defer func() {
        if cerr := closeFile(); cerr != nil && err == nil {
            err = cerr
        }
    }()
    return nil
}

func main() {
    fmt.Println(save())
}
12

Methods & Interfaces

value and pointer receivers, implicit interfaces, type assertions, type switches and nil interfaces

Methods

core

Value and pointer receivers

package main

import "fmt"

type Rect struct{ W, H float64 }

func (r Rect) Area() float64 { return r.W * r.H }

func (r *Rect) Scale(f float64) {
    r.W *= f
    r.H *= f
}

func main() {
    r := Rect{2, 3}
    r.Scale(2)
    fmt.Println(r, r.Area())
}

Methods on any defined type

package main

import (
    "fmt"
    "strings"
)

type Tags []string

func (t Tags) String() string { return "#" + strings.Join(t, " #") }

func (t Tags) Has(s string) bool {
    for _, x := range t {
        if x == s {
            return true
        }
    }
    return false
}

func main() {
    t := Tags{"go", "cli"}
    fmt.Println(t, t.Has("go"))
}

Interfaces

core

Implemented implicitly

package main

import (
    "fmt"
    "math"
)

type Shape interface {
    Area() float64
}

type Circle struct{ R float64 }
type Square struct{ S float64 }

func (c Circle) Area() float64 { return math.Pi * c.R * c.R }
func (s Square) Area() float64 { return s.S * s.S }

func total(shapes ...Shape) (sum float64) {
    for _, s := range shapes {
        sum += s.Area()
    }
    return
}

func main() {
    fmt.Printf("%.2f\n", total(Circle{1}, Square{2}))
}

Type assertions and type switches

func describe(v any) string {
    switch x := v.(type) {
    case nil:
        return "nil"
    case int, int64:
        return fmt.Sprintf("integer %v", x)
    case string:
        return "string of " + strconv.Itoa(len(x)) + " bytes"
    case fmt.Stringer:
        return "stringer " + x.String()
    default:
        return fmt.Sprintf("other %T", x)
    }
}

func main() {
    fmt.Println(describe(42), "|", describe("hi"), "|", describe(time.Second), "|", describe(2.5))
    var v any = "text"
    s, ok := v.(string)
    n, ok2 := v.(int)
    fmt.Println(s, ok, n, ok2)
}

An interface holding a nil pointer is not nil

package main

import "fmt"

type MyErr struct{}

func (*MyErr) Error() string { return "my error" }

func find(fail bool) error {
    var e *MyErr
    if fail {
        e = &MyErr{}
    }
    return e
}

func main() {
    err := find(false)
    fmt.Println(err == nil, err)
}

Pointer methods and method sets

package main

import "fmt"

type Speaker interface{ Speak() string }

type Dog struct{}

func (d *Dog) Speak() string { return "woof" }

func main() {
    var s Speaker = Dog{}
    fmt.Println(s.Speak())
}

Composing interfaces

type Reader interface{ Read(p []byte) (n int, err error) }
type Writer interface{ Write(p []byte) (n int, err error) }
type ReadWriter interface {
    Reader
    Writer
}

var _ io.ReadWriter = (*bytes.Buffer)(nil)
13

Generics

type parameters, constraints, generic types, generic methods and inference

Generic Functions

Go 1.18+core

Type parameters and constraints

package main

import (
    "cmp"
    "fmt"
)

func Map[T, U any](xs []T, f func(T) U) []U {
    out := make([]U, 0, len(xs))
    for _, x := range xs {
        out = append(out, f(x))
    }
    return out
}

func MaxOf[T cmp.Ordered](xs ...T) T {
    m := xs[0]
    for _, x := range xs[1:] {
        m = max(m, x)
    }
    return m
}

func main() {
    lens := Map([]string{"go", "rust", "c"}, func(s string) int { return len(s) })
    fmt.Println(lens, MaxOf(3, 9, 4), MaxOf("pear", "apple"))
}

Your own constraints with unions and ~

package main

import "fmt"

type Number interface {
    ~int | ~int64 | ~float64
}

func Sum[T Number](xs []T) T {
    var total T
    for _, x := range xs {
        total += x
    }
    return total
}

type Cents int64

func main() {
    fmt.Println(Sum([]int{1, 2, 3}), Sum([]float64{0.5, 0.25}), Sum([]Cents{199, 1}))
}

Generic Types

core

A generic stack

package main

import "fmt"

type Stack[T any] struct{ items []T }

func (s *Stack[T]) Push(v T) { s.items = append(s.items, v) }

func (s *Stack[T]) Pop() (T, bool) {
    var zero T
    if len(s.items) == 0 {
        return zero, false
    }
    v := s.items[len(s.items)-1]
    s.items = s.items[:len(s.items)-1]
    return v, true
}

func main() {
    var s Stack[string]
    s.Push("a")
    s.Push("b")
    v, ok := s.Pop()
    fmt.Println(v, ok, len(s.items))
}

Generic methods

package main

import (
    "fmt"
    "strconv"
)

type List struct{ items []int }

func (l List) Map[U any](f func(int) U) []U {
    out := make([]U, len(l.items))
    for i, v := range l.items {
        out[i] = f(v)
    }
    return out
}

func main() {
    l := List{items: []int{1, 2, 3}}
    fmt.Printf("%q\n", l.Map(strconv.Itoa))
    fmt.Println(l.Map(func(v int) bool { return v%2 == 1 }))
}

Constraints that refer to themselves

package main

import "fmt"

type Adder[A Adder[A]] interface {
    Add(A) A
}

type Money int

func (m Money) Add(o Money) Money { return m + o }

func Total[A Adder[A]](xs ...A) A {
    var t A
    for _, x := range xs {
        t = t.Add(x)
    }
    return t
}

func main() {
    fmt.Println(Total(Money(5), Money(7)))
}

Inference when converting a generic function

package main

import "fmt"

func Double[T int | float64](x T) T { return x * 2 }

func main() {
    var f func(float64) float64 = Double
    g := (func(int) int)(Double)
    fmt.Println(f(1.5), g(21))
}

When to Use Generics

everyday

Rules of thumb

Use generics forPrefer instead
Containers: sets, stacks, caches, treesan interface, when behavior differs per type
Functions over slices, maps and channels of any elementa plain function, when there is only one type today
Removing copies of the same algorithm for int, float64, stringcode generation, rarely needed since Go 1.18
Type safe wrappers around any-based APIsreflection, when the type is only known at run time
14

Errors, panic & recover

error values, wrapping, errors.Is, errors.As, AsType, custom errors, Join, panic and recover

Creating and Checking Errors

core

Wrap with %w, check with errors.Is

package main

import (
    "errors"
    "fmt"
    "io/fs"
    "os"
)

var ErrNotFound = errors.New("not found")

func loadUser(id int) error {
    return fmt.Errorf("load user %d: %w", id, ErrNotFound)
}

func main() {
    err := loadUser(7)
    fmt.Println(err)
    fmt.Println(errors.Is(err, ErrNotFound), err == ErrNotFound)
    _, err = os.ReadFile("/no/such/file")
    fmt.Println(errors.Is(err, fs.ErrNotExist))
}

Custom error types and errors.AsType

package main

import (
    "errors"
    "fmt"
)

type ValidationError struct {
    Field string
}

func (e *ValidationError) Error() string { return "invalid " + e.Field }

func save() error {
    return fmt.Errorf("save: %w", &ValidationError{Field: "email"})
}

func main() {
    err := save()
    if ve, ok := errors.AsType[*ValidationError](err); ok {
        fmt.Println("field:", ve.Field)
    }
    var ve *ValidationError
    fmt.Println(errors.As(err, &ve), ve.Field)
}

Several errors at once

err := errors.Join(errors.New("name is empty"), nil, errors.New("age is negative"))
fmt.Println(err)
multi := fmt.Errorf("validate: %w; also %w", io.EOF, os.ErrClosed)
fmt.Println(errors.Is(multi, io.EOF), errors.Is(multi, os.ErrClosed))

panic and recover

everyday

Turn a panic into an error

package main

import "fmt"

func safeDiv(a, b int) (q int, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("recovered: %v", r)
        }
    }()
    return a / b, nil
}

func main() {
    fmt.Println(safeDiv(10, 2))
    fmt.Println(safeDiv(1, 0))
}

Must helpers for startup code

var tmpl = template.Must(template.New("t").Parse("Hello, {{.}}\n"))
var re = regexp.MustCompile(`^\d{3}-\d{4}$`)

func main() {
    if err := tmpl.Execute(os.Stdout, "Gopher"); err != nil {
        log.Fatal(err)
    }
    fmt.Println(re.MatchString("555-0100"))
}

An unrecovered panic in any goroutine kills the program

go func() {
    panic("worker failed")
}()
time.Sleep(time.Second)
fmt.Println("never printed")
15

Iterators

range over functions, iter.Seq, iter.Seq2, pull iterators and the iterator helpers

Range over Functions

Go 1.23+core

An iter.Seq is a function that calls yield

package main

import (
    "fmt"
    "iter"
)

func Countdown(from int) iter.Seq[int] {
    return func(yield func(int) bool) {
        for i := from; i > 0; i-- {
            if !yield(i) {
                return
            }
        }
    }
}

func main() {
    for n := range Countdown(5) {
        if n == 2 {
            break
        }
        fmt.Print(n, " ")
    }
    fmt.Println()
}

Key and value pairs with iter.Seq2

package main

import (
    "fmt"
    "iter"
    "strings"
)

func Words(s string) iter.Seq2[int, string] {
    return func(yield func(int, string) bool) {
        for i, w := range strings.Fields(s) {
            if !yield(i, w) {
                return
            }
        }
    }
}

func main() {
    for i, w := range Words("iterators are just functions") {
        fmt.Println(i, w)
    }
}

Iterator Helpers

Go 1.23+everyday

slices and maps speak iterators

names := []string{"cid", "ann", "bob"}
for i, v := range slices.Backward(names) {
    fmt.Print(i, ":", v, " ")
}
fmt.Println()
sorted := slices.Sorted(slices.Values(names))
upper := slices.Collect(func(yield func(string) bool) {
    for _, n := range sorted {
        if !yield(strings.ToUpper(n)) {
            return
        }
    }
})
fmt.Println(sorted, upper)
m := map[string]int{"a": 1, "b": 2}
fmt.Println(slices.Sorted(maps.Values(m)), len(maps.Collect(maps.All(m))))

A generic Filter

package main

import (
    "fmt"
    "iter"
    "slices"
)

func Filter[T any](seq iter.Seq[T], keep func(T) bool) iter.Seq[T] {
    return func(yield func(T) bool) {
        for v := range seq {
            if keep(v) && !yield(v) {
                return
            }
        }
    }
}

func main() {
    evens := Filter(slices.Values([]int{1, 2, 3, 4, 5, 6}), func(n int) bool { return n%2 == 0 })
    fmt.Println(slices.Collect(evens))
}

Pull iterators

next, stop := iter.Pull(slices.Values([]string{"a", "b", "c"}))
defer stop()
for {
    v, ok := next()
    if !ok {
        break
    }
    fmt.Print(v, " ")
}
fmt.Println()
16

Goroutines & WaitGroup

starting goroutines, waiting for them, GOMAXPROCS and leaks

Starting and Waiting

core

go starts a goroutine, WaitGroup waits

results := make([]string, 3)
var wg sync.WaitGroup
for i, name := range []string{"ann", "bob", "cid"} {
    wg.Go(func() {
        results[i] = strings.ToUpper(name)
    })
}
wg.Wait()
fmt.Println(results)

The classic Add and Done form

var wg sync.WaitGroup
var mu sync.Mutex
total := 0
for i := 1; i <= 100; i++ {
    wg.Add(1)
    go func() {
        defer wg.Done()
        mu.Lock()
        total += i
        mu.Unlock()
    }()
}
wg.Wait()
fmt.Println(total)

How many threads and goroutines

fmt.Println(runtime.NumCPU() > 0, runtime.GOMAXPROCS(0) > 0, runtime.NumGoroutine())

Goroutines Are Cheap

everyday

A hundred thousand goroutines

var wg sync.WaitGroup
var done atomic.Int64
for range 100_000 {
    wg.Go(func() {
        done.Add(1)
    })
}
wg.Wait()
fmt.Println(done.Load(), "goroutines finished")

Leaks and Errors

everyday

A goroutine blocked forever is a leak

func first(urls []string) string {
    ch := make(chan string)
    for _, u := range urls {
        go func() { ch <- fetch(u) }()
    }
    return <-ch
}

errgroup: wait and return the first error

g, ctx := errgroup.WithContext(ctx)
g.SetLimit(8)
for _, url := range urls {
    g.Go(func() error {
        return fetch(ctx, url)
    })
}
if err := g.Wait(); err != nil {
    return err
}
17

Channels & select

unbuffered and buffered channels, close, range, select, timeouts and deadlocks

Channel Basics

core

Unbuffered channels synchronize

ch := make(chan string)
go func() {
    ch <- "ping"
}()
msg := <-ch
fmt.Println(msg)

Buffered channels, close and range

jobs := make(chan int, 3)
jobs <- 1
jobs <- 2
fmt.Println(len(jobs), cap(jobs))
close(jobs)
for j := range jobs {
    fmt.Print(j, " ")
}
v, ok := <-jobs
fmt.Println(v, ok)

Deadlock

ch := make(chan int)
ch <- 1
fmt.Println(<-ch)

select

core

Wait on several channels, with a timeout

fast := make(chan string, 1)
slow := make(chan string, 1)
go func() { time.Sleep(10 * time.Millisecond); fast <- "fast" }()
go func() { time.Sleep(2 * time.Second); slow <- "slow" }()
for range 2 {
    select {
    case m := <-fast:
        fmt.Println(m)
    case m := <-slow:
        fmt.Println(m)
    case <-time.After(200 * time.Millisecond):
        fmt.Println("timeout")
    }
}

Non-blocking send and receive

ch := make(chan int, 1)
for i := range 3 {
    select {
    case ch <- i:
        fmt.Println("sent", i)
    default:
        fmt.Println("full, dropped", i)
    }
}
select {
case v := <-ch:
    fmt.Println("got", v)
default:
    fmt.Println("empty")
}

Direction in types, nil to disable a case

func producer(out chan<- int) {
    for i := range 3 {
        out <- i
    }
    close(out)
}

func consumer(in <-chan int) (sum int) {
    for v := range in {
        sum += v
    }
    return
}

func main() {
    ch := make(chan int)
    go producer(ch)
    fmt.Println(consumer(ch))
    var off chan int
    select {
    case <-off:
        fmt.Println("never")
    default:
        fmt.Println("a nil channel is never ready")
    }
}

Channel Rules

everyday

What each operation does

Operationnil channelopen channelclosed channel
Send ch <- vblocks foreverwaits for room or a receiverpanics
Receive <-chblocks foreverwaits for a valuezero value, ok is false
close(ch)panicsclosespanics
len(ch), cap(ch)0items in buffer, buffer sizeitems left, buffer size
18

Mutexes & Atomics

sync.Mutex, RWMutex, Once, OnceValue, atomics, sync.Map, sync.Pool and the race detector

Mutexes

core

Guard shared state with a Mutex

package main

import (
    "fmt"
    "sync"
)

type Counter struct {
    mu sync.Mutex
    m  map[string]int
}

func (c *Counter) Inc(key string) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.m[key]++
}

func main() {
    c := Counter{m: map[string]int{}}
    var wg sync.WaitGroup
    for range 1000 {
        wg.Go(func() { c.Inc("hits") })
    }
    wg.Wait()
    fmt.Println(c.m["hits"])
}

RWMutex for read-heavy data

type Cache struct {
    mu   sync.RWMutex
    data map[string]string
}

func (c *Cache) Get(k string) (string, bool) {
    c.mu.RLock()
    defer c.mu.RUnlock()
    v, ok := c.data[k]
    return v, ok
}

func (c *Cache) Set(k, v string) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.data[k] = v
}

Once, Atomics, Map and Pool

everyday

Run once, lazily

var config = sync.OnceValue(func() map[string]string {
    fmt.Println("loading config")
    return map[string]string{"env": "prod"}
})

func main() {
    fmt.Println(config()["env"])
    fmt.Println(config()["env"])
}

Typed atomics

var hits atomic.Int64
var ready atomic.Bool
var wg sync.WaitGroup
for range 500 {
    wg.Go(func() { hits.Add(1) })
}
wg.Wait()
ready.Store(true)
fmt.Println(hits.Load(), ready.Load(), hits.CompareAndSwap(500, 0), hits.Load())

sync.Map and sync.Pool

var seen sync.Map
seen.Store("a", 1)
v, loaded := seen.LoadOrStore("a", 2)
fmt.Println(v, loaded)
bufPool := sync.Pool{New: func() any { return new(bytes.Buffer) }}
buf := bufPool.Get().(*bytes.Buffer)
buf.WriteString("reuse me")
fmt.Println(buf.Len())
buf.Reset()
bufPool.Put(buf)

The Race Detector

core

Find data races

go test -race ./...
go run -race .
19

context

cancellation, deadlines, values and causes

Cancellation and Deadlines

core

WithTimeout and ctx.Done

func slowWork(ctx context.Context) error {
    select {
    case <-time.After(time.Second):
        return nil
    case <-ctx.Done():
        return ctx.Err()
    }
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
    defer cancel()
    err := slowWork(ctx)
    fmt.Println(err, errors.Is(err, context.DeadlineExceeded))
}

Cancel a tree of work

ctx, cancel := context.WithCancel(context.Background())
child, stop := context.WithTimeout(ctx, time.Hour)
defer stop()
cancel()
<-child.Done()
fmt.Println(ctx.Err(), child.Err())

Say why with a cause

ctx, cancel := context.WithCancelCause(context.Background())
cancel(errors.New("user logged out"))
fmt.Println(ctx.Err())
fmt.Println(context.Cause(ctx))
tctx, tcancel := context.WithTimeoutCause(context.Background(), time.Millisecond, errors.New("payment API too slow"))
defer tcancel()
<-tctx.Done()
fmt.Println(tctx.Err(), "|", context.Cause(tctx))

Values and Helpers

everyday

Request-scoped values with a private key type

package main

import (
    "context"
    "fmt"
)

type ctxKey struct{}

func WithUser(ctx context.Context, name string) context.Context {
    return context.WithValue(ctx, ctxKey{}, name)
}

func User(ctx context.Context) (string, bool) {
    name, ok := ctx.Value(ctxKey{}).(string)
    return name, ok
}

func main() {
    ctx := WithUser(context.Background(), "ann")
    fmt.Println(User(ctx))
    fmt.Println(User(context.Background()))
}

AfterFunc and WithoutCancel

ctx, cancel := context.WithCancel(context.Background())
done := make(chan struct{})
context.AfterFunc(ctx, func() {
    fmt.Println("cleanup after cancel")
    close(done)
})
detached := context.WithoutCancel(ctx)
cancel()
<-done
fmt.Println(ctx.Err(), detached.Err())

Rules

DoDo not
Pass ctx as the first parameter, named ctxstore a context in a struct field
Use r.Context() in HTTP handlers and t.Context() in testscall context.Background() deep inside library code
Call cancel on every path, usually with deferpass a nil context: use context.TODO() while refactoring
Check ctx.Err() or select on ctx.Done() in long loopsuse context values for optional function parameters
20

Concurrency Patterns

worker pools, pipelines, fan-in, semaphores, rate limits and timeouts

Worker Pool

core

A fixed number of workers reading from one channel

package main

import (
    "fmt"
    "slices"
    "sync"
)

func main() {
    jobs := make(chan int)
    results := make(chan int)
    var wg sync.WaitGroup
    for range 3 {
        wg.Go(func() {
            for n := range jobs {
                results <- n * n
            }
        })
    }
    go func() {
        for i := 1; i <= 5; i++ {
            jobs <- i
        }
        close(jobs)
    }()
    go func() {
        wg.Wait()
        close(results)
    }()
    var out []int
    for r := range results {
        out = append(out, r)
    }
    slices.Sort(out)
    fmt.Println(out)
}

Pipeline stages connected by channels

package main

import "fmt"

func gen(nums ...int) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for _, n := range nums {
            out <- n
        }
    }()
    return out
}

func square(in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for n := range in {
            out <- n * n
        }
    }()
    return out
}

func main() {
    for v := range square(square(gen(1, 2, 3))) {
        fmt.Print(v, " ")
    }
    fmt.Println()
}

Limiting Concurrency

everyday

A buffered channel as a semaphore

sem := make(chan struct{}, 2)
var running, peak atomic.Int32
var wg sync.WaitGroup
for range 6 {
    wg.Go(func() {
        sem <- struct{}{}
        defer func() { <-sem }()
        n := running.Add(1)
        for {
            p := peak.Load()
            if n <= p || peak.CompareAndSwap(p, n) {
                break
            }
        }
        time.Sleep(10 * time.Millisecond)
        running.Add(-1)
    })
}
wg.Wait()
fmt.Println("most at once:", peak.Load())

Fan-in: merge several channels

func merge[T any](chans ...<-chan T) <-chan T {
    out := make(chan T)
    var wg sync.WaitGroup
    for _, c := range chans {
        wg.Go(func() {
            for v := range c {
                out <- v
            }
        })
    }
    go func() { wg.Wait(); close(out) }()
    return out
}

func main() {
    a, b := make(chan string, 2), make(chan string, 2)
    a <- "a1"
    a <- "a2"
    b <- "b1"
    close(a)
    close(b)
    var got []string
    for v := range merge(a, b) {
        got = append(got, v)
    }
    slices.Sort(got)
    fmt.Println(got)
}

Rate limit with a ticker

tick := time.NewTicker(20 * time.Millisecond)
defer tick.Stop()
start := time.Now()
for i := range 3 {
    <-tick.C
    fmt.Println("request", i)
}
fmt.Println(time.Since(start) >= 60*time.Millisecond)
21

fmt & slog

Printf verbs, width and precision, Stringer, tabwriter, log and structured logging with slog

Printf Verbs

core

The verbs you use every day

VerbPrintsExample output
%vdefault format{ann 31}
%+vstructs with field names{Name:ann Age:31}
%#vGo syntaxmain.User{Name:"ann", Age:31}
%Tthe typemain.User
%d %b %o %xinteger in base 10, 2, 8, 1642 101010 52 2a
%f %.2f %e %gfloat, fixed, exponent, compact3.141593 3.14 3.141593e+00 3.14159
%s %qstring, quoted stringgopher "gopher"
%tbooltrue
%c %Urune, Unicode code pointG U+0047
%ppointer address0xc000012345
%wwrapped error, fmt.Errorf onlysame as %v when printed
%%a literal percent sign%

See them run

type User struct {
    Name string
    Age  int
}

func main() {
    u := User{"ann", 31}
    fmt.Printf("%v | %+v | %#v | %T\n", u, u, u, u)
    fmt.Printf("%d %b %o %x %X %#x\n", 42, 42, 42, 255, 255, 255)
    fmt.Printf("%f %.2f %e %g %g\n", math.Pi, math.Pi, math.Pi, math.Pi, 1e21)
    fmt.Printf("%s %q %v %c %U\n", "go", "go", []string{"a", "b"}, 'G', 'G')
    fmt.Printf("%t %v %d\n", true, nil, []int{1, 2})
}

Width, precision and alignment

fmt.Printf("[%8s][%-8s][%08.3f][%+d][%x]\n", "right", "left", 3.14159, 5, "hi")
fmt.Printf("[%*d][%-*d]\n", 5, 42, 5, 42)
fmt.Printf("%[2]s %[1]s %[2]s\n", "world", "hello")
fmt.Printf("%6.2f%%\n", 12.3456)

Custom Formatting

everyday

String methods and tables with tabwriter

package main

import (
    "fmt"
    "os"
    "text/tabwriter"
)

type Money int64

func (m Money) String() string { return fmt.Sprintf("$%d.%02d", m/100, m%100) }

func main() {
    fmt.Println(Money(123456), []Money{5, 1999})
    w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
    fmt.Fprintln(w, "NAME\tQTY\tPRICE")
    fmt.Fprintln(w, "gopher plush\t2\t$19.99")
    fmt.Fprintln(w, "sticker\t10\t$1.50")
    w.Flush()
}

Sprint, Sscan and Appendf

s := fmt.Sprint("a", 1, 2, "b")
line := fmt.Sprintln("a", 1)
var name string
var age int
n, err := fmt.Sscan("ann 31", &name, &age)
buf := fmt.Appendf(nil, "id=%d", 7)
fmt.Printf("%q %q %v %v %s %d %s\n", s, line, n, err, name, age, buf)

Logging with slog

Go 1.21+core

Structured logs as JSON

opts := &slog.HandlerOptions{
    Level: slog.LevelDebug,
    ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
        if a.Key == slog.TimeKey {
            return slog.Attr{}
        }
        return a
    },
}
logger := slog.New(slog.NewJSONHandler(os.Stdout, opts))
logger.Info("order placed", "id", 1042, "total", 59.90)
reqLog := logger.With("request_id", "r-77")
reqLog.Warn("slow query", slog.Duration("took", 1500*time.Millisecond))
reqLog.Debug("cache", slog.Group("stats", "hits", 9, "misses", 1))

Text logs, levels and the default logger

h := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
    ReplaceAttr: func(_ []string, a slog.Attr) slog.Attr {
        if a.Key == slog.TimeKey {
            return slog.Attr{}
        }
        return a
    },
})
slog.SetDefault(slog.New(h))
slog.Info("server started", "port", 8080)
slog.Debug("hidden at the default Info level")
log.Printf("the log package now goes through slog too")
22

Files, OS & Processes

reading and writing files, bufio, io.Reader, paths, os.Root, embed, flags, environment and commands

Reading and Writing Files

core

Whole files at once

dir, err := os.MkdirTemp("", "demo")
if err != nil {
    log.Fatal(err)
}
defer os.RemoveAll(dir)
path := filepath.Join(dir, "notes.txt")
if err := os.WriteFile(path, []byte("line one\nline two\n"), 0o644); err != nil {
    log.Fatal(err)
}
data, err := os.ReadFile(path)
if err != nil {
    log.Fatal(err)
}
fmt.Printf("%d bytes: %q\n", len(data), data)
_, err = os.ReadFile(filepath.Join(dir, "missing.txt"))
fmt.Println(errors.Is(err, fs.ErrNotExist))

Line by line with bufio.Scanner

r := strings.NewReader("alpha\nbeta\n\ngamma")
sc := bufio.NewScanner(r)
n := 0
for sc.Scan() {
    n++
    fmt.Printf("%d: %q\n", n, sc.Text())
}
if err := sc.Err(); err != nil {
    fmt.Println("read error:", err)
}

Streams: io.Reader, io.Writer and io.Copy

var out bytes.Buffer
src := io.LimitReader(strings.NewReader("stream me, but not all of me"), 9)
n, err := io.Copy(&out, src)
fmt.Println(n, err, out.String())
h := sha256.New()
w := io.MultiWriter(os.Stdout, h)
fmt.Fprint(w, "hash me\n")
fmt.Printf("%x\n", h.Sum(nil)[:4])

Create, append and flush

f, err := os.OpenFile("app.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
    return err
}
defer f.Close()
bw := bufio.NewWriter(f)
fmt.Fprintf(bw, "%s started\n", time.Now().Format(time.RFC3339))
return bw.Flush()

Paths and Directories

everyday

filepath and WalkDir

dir, _ := os.MkdirTemp("", "tree")
defer os.RemoveAll(dir)
for _, p := range []string{"a/x.go", "a/b/y.go", "c.txt"} {
    full := filepath.Join(dir, p)
    os.MkdirAll(filepath.Dir(full), 0o755)
    os.WriteFile(full, nil, 0o644)
}
filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
    if err == nil && filepath.Ext(path) == ".go" {
        rel, _ := filepath.Rel(dir, path)
        fmt.Println(filepath.ToSlash(rel), filepath.Base(path))
    }
    return err
})

os.Root stops path traversal

dir, _ := os.MkdirTemp("", "uploads")
defer os.RemoveAll(dir)
root, err := os.OpenRoot(dir)
if err != nil {
    log.Fatal(err)
}
defer root.Close()
fmt.Println(root.WriteFile("ok.txt", []byte("fine"), 0o644))
_, err = root.Open("../../etc/passwd")
fmt.Println(err)

Embed files in the binary

import "embed"

//go:embed templates/*.html static
var assets embed.FS

//go:embed version.txt
var version string

func main() {
    page, _ := assets.ReadFile("templates/index.html")
    http.Handle("/static/", http.FileServerFS(assets))
}

Arguments, Environment and Commands

core

Flags with the flag package

set := flag.NewFlagSet("server", flag.ContinueOnError)
port := set.Int("port", 8080, "port to listen on")
verbose := set.Bool("v", false, "verbose logging")
var name string
set.StringVar(&name, "name", "api", "service name")
err := set.Parse([]string{"-port", "9090", "-v", "input.csv", "extra"})
fmt.Println(err, *port, *verbose, name, set.Args())

Environment variables

os.Setenv("APP_MODE", "prod")
mode := os.Getenv("APP_MODE")
_, set := os.LookupEnv("NOT_SET_ANYWHERE")
port := cmp.Or(os.Getenv("PORT"), "8080")
fmt.Println(mode, set, port, len(os.Args) > 0)

Run a command

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, "git", "rev-parse", "--short", "HEAD")
out, err := cmd.Output()
if err != nil {
    var ee *exec.ExitError
    if errors.As(err, &ee) {
        log.Printf("git failed: %s", ee.Stderr)
    }
    return err
}
fmt.Println(strings.TrimSpace(string(out)))

Stop cleanly on Ctrl+C

ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
go runServer(ctx)
<-ctx.Done()
log.Println("shutting down:", context.Cause(ctx))
23

JSON

Marshal and Unmarshal, struct tags, omitempty and omitzero, streaming, custom marshaling and encoding/json/v2

Marshal and Unmarshal

core

Struct tags control the names

type Item struct {
    SKU      string    `json:"sku"`
    Price    float64   `json:"price"`
    Tags     []string  `json:"tags,omitempty"`
    Internal string    `json:"-"`
    Added    time.Time `json:"added,omitzero"`
    note     string
}

func main() {
    b, err := json.Marshal(Item{SKU: "g-1", Price: 9.5, Internal: "x", note: "y"})
    fmt.Println(string(b), err)
    pretty, _ := json.MarshalIndent(map[string]int{"b": 2, "a": 1}, "", "  ")
    fmt.Println(string(pretty))
}

Decode into a struct

type Order struct {
    ID    int      `json:"id"`
    Items []string `json:"items"`
    Paid  bool     `json:"paid"`
}

func main() {
    var o Order
    err := json.Unmarshal([]byte(`{"id": 7, "ITEMS": ["a", "b"], "extra": true}`), &o)
    fmt.Printf("%+v %v\n", o, err)
    err = json.Unmarshal([]byte(`{"id": "seven"}`), &o)
    fmt.Println(err)
}

Unknown shape: map[string]any

var v map[string]any
json.Unmarshal([]byte(`{"n": 42, "list": [1, "two"], "obj": {"ok": true}}`), &v)
fmt.Printf("%T %v | %T | %v\n", v["n"], v["n"], v["list"], v["obj"].(map[string]any)["ok"])
d := json.NewDecoder(strings.NewReader(`{"id": 12345678901234567890}`))
d.UseNumber()
var w map[string]any
d.Decode(&w)
fmt.Println(w["id"])

nil slices become null

type Resp struct {
    Items []string `json:"items"`
}
var a, b Resp
b.Items = []string{}
x, _ := json.Marshal(a)
y, _ := json.Marshal(b)
fmt.Println(string(x), string(y))

Streaming and Custom Types

everyday

Encoder and Decoder for streams

in := strings.NewReader(`{"n":1} {"n":2} {"n":3}`)
dec := json.NewDecoder(in)
dec.DisallowUnknownFields()
enc := json.NewEncoder(os.Stdout)
for {
    var m struct{ N int }
    if err := dec.Decode(&m); err == io.EOF {
        break
    } else if err != nil {
        fmt.Println(err)
        break
    }
    m.N *= 10
    enc.Encode(m)
}

MarshalJSON and UnmarshalJSON

package main

import (
    "encoding/json"
    "fmt"
    "strings"
)

type Level int

func (l Level) MarshalJSON() ([]byte, error) {
    return json.Marshal([]string{"low", "high"}[l])
}

func (l *Level) UnmarshalJSON(b []byte) error {
    var s string
    if err := json.Unmarshal(b, &s); err != nil {
        return err
    }
    if strings.EqualFold(s, "high") {
        *l = 1
    } else {
        *l = 0
    }
    return nil
}

func main() {
    b, _ := json.Marshal(map[string]Level{"alert": 1})
    var back map[string]Level
    err := json.Unmarshal([]byte(`{"x": "HIGH"}`), &back)
    fmt.Println(string(b), back["x"], err)
}

encoding/json/v2

Go 1.27+advanced

Stricter defaults

type T struct {
    Name string
    Tags []string
}

func main() {
    v2, _ := jsonv2.Marshal(T{Name: "x"})
    fmt.Println(string(v2))
    var t T
    fmt.Println(jsonv2.Unmarshal([]byte(`{"Name":"a","Name":"b"}`), &t))
    var u T
    fmt.Println(jsonv2.Unmarshal([]byte(`{"name":"lower"}`), &u), u.Name == "")
    fmt.Println(jsonv2.Unmarshal([]byte(`{"name":"lower"}`), &u, jsonv2.MatchCaseInsensitiveNames(true)), u.Name)
}

Tokens with jsontext

dec := jsontext.NewDecoder(strings.NewReader(`{"a": [1, 2], "b": null}`))
for {
    tok, err := dec.ReadToken()
    if err != nil {
        break
    }
    fmt.Print(tok.Kind(), " ")
}
fmt.Println()
24

net/http

servers with ServeMux patterns, handlers, middleware, JSON APIs, clients and graceful shutdown

Server and Routing

core

Method and path patterns

mux := http.NewServeMux()
mux.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "user %s", r.PathValue("id"))
})
mux.HandleFunc("POST /users", func(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusCreated)
})
mux.HandleFunc("GET /files/{path...}", func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprint(w, "file ", r.PathValue("path"))
})
for _, t := range [][2]string{{"GET", "/users/42"}, {"POST", "/users"}, {"DELETE", "/users/42"}, {"GET", "/files/a/b.txt"}} {
    rec := httptest.NewRecorder()
    mux.ServeHTTP(rec, httptest.NewRequest(t[0], t[1], nil))
    fmt.Println(t[0], t[1], rec.Code, rec.Body.String())
}

A JSON endpoint

package main

import (
    "encoding/json"
    "fmt"
    "net/http"
    "net/http/httptest"
    "strings"
)

type CreateReq struct {
    Name string `json:"name"`
}

func create(w http.ResponseWriter, r *http.Request) {
    var req CreateReq
    r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Name == "" {
        http.Error(w, "name is required", http.StatusBadRequest)
        return
    }
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(map[string]any{"id": 1, "name": req.Name})
}

func main() {
    for _, body := range []string{`{"name":"ann"}`, `{}`} {
        rec := httptest.NewRecorder()
        create(rec, httptest.NewRequest("POST", "/users", strings.NewReader(body)))
        fmt.Print(rec.Code, " ", rec.Body.String())
    }
}

Middleware wraps a handler

package main

import (
    "fmt"
    "net/http"
    "net/http/httptest"
)

func requireKey(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if r.Header.Get("X-API-Key") != "secret" {
            http.Error(w, "forbidden", http.StatusForbidden)
            return
        }
        next.ServeHTTP(w, r)
    })
}

func main() {
    hello := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, "hello") })
    h := requireKey(hello)
    for _, key := range []string{"", "secret"} {
        req := httptest.NewRequest("GET", "/", nil)
        req.Header.Set("X-API-Key", key)
        rec := httptest.NewRecorder()
        h.ServeHTTP(rec, req)
        fmt.Printf("%d %q\n", rec.Code, rec.Body.String())
    }
}

Production Server

everyday

Timeouts and graceful shutdown

srv := &http.Server{
    Addr:              ":8080",
    Handler:           mux,
    ReadHeaderTimeout: 5 * time.Second,
    ReadTimeout:       30 * time.Second,
    WriteTimeout:      30 * time.Second,
    IdleTimeout:       2 * time.Minute,
}
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
go func() {
    if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
        log.Fatal(err)
    }
}()
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
srv.Shutdown(shutdownCtx)

CSRF protection without tokens

csrf := http.NewCrossOriginProtection()
csrf.AddTrustedOrigin("https://admin.example.com")
handler := csrf.Handler(mux)

HTTP Client

core

A client with a timeout and a context

client := &http.Client{Timeout: 10 * time.Second}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.example.com/items?page=2", nil)
if err != nil {
    return err
}
req.Header.Set("Accept", "application/json")
resp, err := client.Do(req)
if err != nil {
    return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
    return fmt.Errorf("unexpected status %s", resp.Status)
}
var items []Item
return json.NewDecoder(resp.Body).Decode(&items)

Build URLs safely

u, _ := url.Parse("https://api.example.com/search")
q := u.Query()
q.Set("q", "go & rust")
q.Set("page", "2")
u.RawQuery = q.Encode()
fmt.Println(u.String())
fmt.Println(url.PathEscape("a b/c"), url.QueryEscape("a b/c"))
25

Time & Dates

formatting with the reference time, parsing, time zones, durations, Unix times, timers and tickers

Formatting and Parsing

core

Layouts use the reference time Mon Jan 2 15:04:05 MST 2006

t := time.Date(2026, time.September, 29, 14, 5, 9, 0, time.UTC)
fmt.Println(t.Format("2006-01-02 15:04:05"))
fmt.Println(t.Format("Jan 2, 2006 at 3:04pm"))
fmt.Println(t.Format(time.RFC3339), t.Format(time.DateOnly), t.Format(time.Kitchen))
fmt.Println(t.Format("Monday 02/01/06 _2 002"))

Parse with a layout

t, err := time.Parse("2006-01-02 15:04", "2026-09-29 18:30")
fmt.Println(t, err)
_, err = time.Parse(time.DateOnly, "2026-13-01")
fmt.Println(err)
loc, _ := time.LoadLocation("Europe/Bucharest")
local, _ := time.ParseInLocation(time.DateTime, "2026-09-29 18:30:00", loc)
fmt.Println(local, local.UTC())

The layout tokens

TokenMeaningTokenMeaning
2006 06year15 03 3hour, 24h or 12h
01 1 Jan Januarymonth04 4minute
02 2 _2day of month05 5second
002day of year.000 .999fraction, fixed or trimmed
Mon MondayweekdayPM pmAM/PM marker
MSTzone name-0700 -07:00 Z07:00zone offset

Durations and Arithmetic

core

time.Duration is nanoseconds

d := 90*time.Minute + 30*time.Second
fmt.Println(d, d.Hours(), d.Round(time.Hour), d.Truncate(time.Hour))
p, _ := time.ParseDuration("1h15m30.5s")
fmt.Println(p, p.Seconds())
n := 3
fmt.Println(time.Duration(n) * time.Second)

Add, compare and difference

start := time.Date(2026, 1, 31, 9, 0, 0, 0, time.UTC)
fmt.Println(start.Add(36*time.Hour), start.AddDate(0, 1, 0))
end := start.Add(90 * time.Minute)
fmt.Println(end.Sub(start), end.After(start), start.Equal(start.In(time.Local)))
fmt.Println(start.Weekday(), start.YearDay(), start.Unix(), time.Unix(0, 0).UTC())

Timers and Tickers

everyday

Timer, AfterFunc and Ticker

timer := time.NewTimer(50 * time.Millisecond)
stopped := timer.Stop()
fmt.Println("stopped before firing:", stopped)
done := make(chan bool)
time.AfterFunc(10*time.Millisecond, func() { done <- true })
fmt.Println("AfterFunc ran:", <-done)
ticker := time.NewTicker(10 * time.Millisecond)
defer ticker.Stop()
for i := range 3 {
    <-ticker.C
    fmt.Println("tick", i)
}

Wall clock and monotonic clock

start := time.Now()
time.Sleep(20 * time.Millisecond)
elapsed := time.Since(start)
fmt.Println(elapsed >= 20*time.Millisecond, strings.Contains(start.String(), "m=+"))
fmt.Println(strings.Contains(start.Round(0).String(), "m=+"))
26

Modules & Workspaces

go.mod, dependencies, versions, replace, workspaces, tool dependencies and private modules

go.mod and Dependencies

core

A typical go.mod

module github.com/acme/shop

go 1.27.0

toolchain go1.27.1

require (
    github.com/jackc/pgx/v5 v5.11.0
    golang.org/x/sync v0.23.0
)

require golang.org/x/text v0.42.0 // indirect

tool golang.org/x/tools/cmd/stringer

Add, upgrade and remove dependencies

go get github.com/jackc/pgx/v5            # add or upgrade to the latest release
go get github.com/jackc/pgx/v5@v5.11.0     # a specific version
go get golang.org/x/sync@none             # remove it
go get -u ./...                           # upgrade every dependency, minor and patch
go get -u=patch ./...                     # patch releases only
go mod tidy                               # add what is imported, drop what is not
go list -m -u all                         # which modules have updates
go mod why -m golang.org/x/text           # why is this module needed

Major versions live in the import path

import (
    "github.com/jackc/pgx/v5"
    "gopkg.in/yaml.v3"
)

Local Development

everyday

replace and workspaces

go mod edit -replace github.com/acme/lib=../lib   # this module only
go mod edit -dropreplace github.com/acme/lib

go work init ./shop ./lib                        # go.work: several modules at once
go work use ./tools
go work sync

Tool dependencies

go get -tool golang.org/x/tools/cmd/stringer@latest
go tool stringer -type=Color
go get -tool honnef.co/go/tools/cmd/staticcheck@2026.2.1
go tool staticcheck ./...
go get -u tool                                   # upgrade all tools

Private modules and proxies

go env -w GOPRIVATE=github.com/acme/*,gitlab.corp.example.com
go env -w GOPROXY=https://proxy.golang.org,direct
git config --global url."git@github.com:acme/".insteadOf "https://github.com/acme/"
GOFLAGS=-mod=mod go build ./...
go mod vendor                                    # copy dependencies into vendor/

Versions in Code

advanced

Compare Go versions with go/version

fmt.Println(version.Compare("go1.26.8", "go1.27.1"), version.Compare("go1.27", "go1.27.0"))
fmt.Println(version.Lang("go1.27.1"), version.IsValid("go1.27rc1"), version.IsValid("1.27"))
27

Testing & Benchmarks

table tests, subtests, helpers, benchmarks with b.Loop, fuzzing, examples, synctest and coverage

Unit Tests

core

A table driven test

package mathx

import "testing"

func TestAbs(t *testing.T) {
    tests := []struct {
        name string
        in   int
        want int
    }{
        {"positive", 3, 3},
        {"negative", -3, 3},
        {"zero", 0, 0},
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            if got := Abs(tt.in); got != tt.want {
                t.Errorf("Abs(%d) = %d, want %d", tt.in, got, tt.want)
            }
        })
    }
}

Run them

go test ./...
go test -v -run 'TestAbs/negative' ./mathx
go test -race -count=1 ./...
go test -cover ./...

Helpers, cleanup, temp dirs and the test context

func newStore(t *testing.T) *Store {
    t.Helper()
    dir := t.TempDir()
    t.Setenv("STORE_MODE", "test")
    s, err := Open(t.Context(), filepath.Join(dir, "db"))
    if err != nil {
        t.Fatalf("open store: %v", err)
    }
    t.Cleanup(func() { s.Close() })
    return s
}

Test HTTP handlers with httptest

func TestHealth(t *testing.T) {
    rec := httptest.NewRecorder()
    req := httptest.NewRequest(http.MethodGet, "/health", nil)
    healthHandler(rec, req)
    if rec.Code != http.StatusOK {
        t.Fatalf("status = %d, want 200", rec.Code)
    }
}

Benchmarks, Fuzzing and Examples

everyday

Benchmarks with b.Loop

func BenchmarkJoin(b *testing.B) {
    parts := []string{"a", "b", "c", "d"}
    for b.Loop() {
        strings.Join(parts, ",")
    }
}

go test -run='^$' -bench=Join -benchmem ./...

Fuzz tests find inputs you did not think of

func FuzzReverse(f *testing.F) {
    f.Add("hello")
    f.Fuzz(func(t *testing.T, s string) {
        r := Reverse(Reverse(s))
        if utf8.ValidString(s) && r != s {
            t.Errorf("Reverse twice changed %q to %q", s, r)
        }
    })
}

go test -fuzz=FuzzReverse -fuzztime=30s ./strx

Examples are tests and documentation

func ExampleAbs() {
    fmt.Println(Abs(-5))
    // Output: 5
}

Fake time with testing/synctest

func TestTimeout(t *testing.T) {
    synctest.Test(t, func(t *testing.T) {
        ctx, cancel := context.WithTimeout(t.Context(), time.Hour)
        defer cancel()
        synctest.Sleep(time.Hour)
        if ctx.Err() == nil {
            t.Fatal("expected the hour-long timeout to fire")
        }
    })
}

Allocations

advanced

Count allocations without a benchmark run

parts := []string{"a", "b", "c", "d", "e", "f", "g", "h"}
plus := testing.AllocsPerRun(100, func() {
    s := ""
    for _, p := range parts {
        s += p
    }
    _ = s
})
builder := testing.AllocsPerRun(100, func() {
    var b strings.Builder
    b.Grow(16)
    for _, p := range parts {
        b.WriteString(p)
    }
    _ = b.String()
})
fmt.Println("with +:", plus, "with Builder:", builder)
28

Tools & Profiling

gofmt, go vet, go fix, linters, govulncheck, pprof, traces, Delve, build tags, cross compiling and PGO

Formatting and Static Checks

core

The standard toolchain checks

gofmt -l -w .                 # format files in place, list changed ones
go vet ./...                  # printf, copylocks, loopclosure, structtag and more
go fix ./...                  # apply modernizers: any, min/max, range over int, ...
go fix -diff ./...            # preview instead of writing

Linters and vulnerability scanning

go install honnef.co/go/tools/cmd/staticcheck@2026.2.1
staticcheck ./...
golangci-lint run ./...       # many linters in one run, configured in .golangci.yml
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./...

Inline a deprecated function for callers

//go:fix inline
func OldName(s string) string { return NewName(s) }

Profiling

everyday

CPU and memory profiles

import _ "net/http/pprof"

go func() { log.Println(http.ListenAndServe("localhost:6060", nil)) }()

go tool pprof -http=:8081 "http://localhost:6060/debug/pprof/profile?seconds=30"
go tool pprof -http=:8081 http://localhost:6060/debug/pprof/heap
go test -run='^$' -bench=. -cpuprofile=cpu.out ./parser

Execution traces and the flight recorder

fr := trace.NewFlightRecorder(trace.FlightRecorderConfig{MinAge: 5 * time.Second})
fr.Start()
defer fr.Stop()
if slowRequest {
    f, _ := os.Create("slow.trace")
    fr.WriteTo(f)
    f.Close()
}

go tool trace slow.trace

Debug with Delve

go install github.com/go-delve/delve/cmd/dlv@latest
dlv debug ./cmd/api -- -port 8080
(dlv) break orders.go:42
(dlv) continue
(dlv) print order.Total
(dlv) goroutines

Builds

everyday

Build constraints and file names

//go:build linux && !cgo

package netx

Cross compile and stamp a version

GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -o bin/api ./cmd/api
GOOS=windows GOARCH=amd64 go build -o bin/api.exe ./cmd/api
go build -ldflags="-s -w -X main.version=1.4.0" ./cmd/api
go version -m bin/api
go tool dist list | grep linux

Profile guided optimization

curl -o cmd/api/default.pgo "http://localhost:6060/debug/pprof/profile?seconds=60"
go build ./cmd/api

Go Tools as Libraries

advanced

What gofmt does, from code

src := []byte("package main\nimport \"fmt\"\nfunc main(){fmt.Println( \"hi\" )\nx:=[]int{1,2,\n3}\n_=x}")
out, err := format.Source(src)
fmt.Println(string(out), err)

Evaluate a build constraint

expr, err := constraint.Parse("//go:build linux && (amd64 || arm64) && !cgo")
if err != nil {
    log.Fatal(err)
}
for _, tags := range [][]string{{"linux", "arm64"}, {"linux", "amd64", "cgo"}, {"darwin", "arm64"}} {
    has := map[string]bool{}
    for _, t := range tags {
        has[t] = true
    }
    fmt.Println(tags, expr.Eval(func(tag string) bool { return has[tag] }))
}
29

Errors & Gotchas

compiler errors, runtime panics and the surprises that catch Go developers

Compiler Errors

core

The errors every Go beginner meets

package main

import (
    "fmt"
    "os"
)

func half(n int) int {
    if n > 0 {
        return n / 2
    }
}

func main() {
    count := 3
    var ratio float64 = 1.5
    fmt.Println(ratio * count)
    fmt.Println(totl)
}

What each message means

MessageUsual fix
declared and not used: xuse it, delete it, or assign to _ while drafting
"os" imported and not useddelete the import; goimports does it on save
missing returnadd a return at the end, or a panic for impossible paths
invalid operation: mismatched typesconvert explicitly: float64(n) * ratio
cannot use x (variable of type T) as U valueconvert, or implement the missing method
undefined: nametypo, unexported name from another package, or missing import
x declared and not used after := in ifthe := made a new variable: use = to assign
assignment mismatch: 2 variables but f returns 1 valuematch the number of results

Runtime Panics

core

Index out of range

s := []int{1, 2, 3}
i := 5
fmt.Println(s[i])

Failed type assertion

var v any = "text"
n := v.(int)
fmt.Println(n)

Close a channel twice

ch := make(chan int)
close(ch)
close(ch)

Surprises

everyday

range gives you a copy

type Item struct{ Qty int }
items := []Item{{1}, {2}}
for _, it := range items {
    it.Qty *= 10
}
fmt.Println(items)
for i := range items {
    items[i].Qty *= 10
}
fmt.Println(items)

:= in an inner scope hides err

func load() (err error) {
    if true {
        _, err := strconv.Atoi("bad")
        _ = err
    }
    return err
}

func main() {
    fmt.Println(load())
}

Unexported fields are invisible to encoding/json

type user struct {
    name  string
    Email string
}
b, _ := json.Marshal(user{name: "ann", Email: "a@example.com"})
fmt.Println(string(b))

A defer in a loop waits for the function

for _, name := range files {
    f, err := os.Open(name)
    if err != nil {
        return err
    }
    defer f.Close()
    process(f)
}
30

Go 1.21 to 1.27

what each release added, the release cycle, support policy and compatibility

What Each Release Added

core

Language and library highlights

ReleaseDateHighlights
Go 1.27Aug 2026generic methods, promoted field names in struct literals, encoding/json/v2, uuid and crypto/mldsa packages, strings.CutLast, goroutineleak profile
Go 1.26Feb 2026new(expr), self-referential generic constraints, Green Tea GC by default, errors.AsType, go fix modernizers, crypto/hpke
Go 1.25Aug 2025sync.WaitGroup.Go, testing/synctest, container-aware GOMAXPROCS, trace flight recorder, http.CrossOriginProtection
Go 1.24Feb 2025generic type aliases, tool directives, testing.B.Loop, os.Root, weak pointers, runtime.AddCleanup, omitzero, Swiss table maps
Go 1.23Aug 2024range over functions, package iter, slices and maps iterator helpers, unique package, timer changes
Go 1.22Feb 2024per-iteration loop variables, range over int, ServeMux method and wildcard patterns, math/rand/v2
Go 1.21Aug 2023min, max and clear built-ins, log/slog, slices, maps and cmp packages, toolchain management, PGO by default

Check and upgrade

go version
go get go@1.27.1 toolchain@go1.27.1
go mod tidy
go fix ./...
go vet ./...

Release Cycle and Support

everyday

Two releases a year, two supported

FactDetail
Major releasesevery six months, in February and August
Supported releasesthe two most recent: Go 1.27 and Go 1.26 today
Minor releasessecurity and bug fixes, for example Go 1.27.1 on September 1, 2026
CompatibilityGo 1 programs keep compiling and running; behavior changes go behind GODEBUG settings tied to the go line
Next releaseGo 1.28, expected in February 2027

GODEBUG keeps old behavior while you migrate

//go:debug http2client=0
package main

godebug (
    default=go1.26
    tlsmlkem=0
)

What Is a Go Cheat Sheet

A Go cheat sheet is a compact reference to the syntax, types, standard library and tooling of the Go programming language, arranged so you find the right form of a slice operation, a select statement or a go test flag in seconds. People also search for a Golang cheat sheet: Golang is the common nickname that comes from the old golang.org domain, and it means the same language.

This Go cheat sheet covers 30 sections and 216 snippets for Go 1.21 through Go 1.27, the current release. Every snippet that needs a newer release carries a version chip, and the version picker in the toolbar hides what your go line cannot build. Rows that need modules outside the standard library carry a chip as well, and stdlib only mode hides them. The output shown under the example programs is what they printed when compiled and run on Compiler Explorer, with the Go development toolchain for Go 1.27 features and Go 1.26 for the rest.

The toolbox at the top lints Go code for the classic mistakes, checks Printf verbs against their arguments like go vet, turns JSON into Go structs with tags, computes struct layout and padding for 64-bit and 32-bit targets, converts date formats into Go's reference time layouts, flags regular expression features that Go's RE2 engine does not have, and explains a go.mod file. The builders write go build commands for cross compiling, multi-stage Dockerfiles, go test commands, GitHub Actions workflows, and profiling and Delve sessions. Everything runs in your browser.

How It Differs From the Tour of Go

The Tour of Go teaches the language step by step; this page is for when you already write Go and need the exact form of something, next to the mistake that usually comes with it: the nil map write, the defer in a loop, the typed nil error, the slice that shares its backing array. It works best alongside the compiler, go vet and a linter such as staticcheck. For the systems language on the other side of the tradeoff, see the Rust cheat sheet; for the language Go borrowed its syntax from, the C cheat sheet.


What This Go Cheat Sheet Covers

The 30 sections go from installing Go to shipping and profiling a service, and each link jumps to that section above.


Go Syntax Quick Reference

The forms people look up most often, one line each. The sections above show each of them in a full example with its output.

TaskGo
Declare and assignname := "gopher"
Declare with a typevar count int64
Constant enumconst ( A = iota; B; C )
Function with an errorfunc load(id int) (*User, error)
Check an errorif err != nil { return fmt.Errorf("load: %w", err) }
Slice literalnums := []int{1, 2, 3}
Appendnums = append(nums, 4)
Map literalages := map[string]int{"ann": 31}
Map lookupv, ok := ages["bob"]
Loop n timesfor i := range n { }
Loop over a slicefor i, v := range nums { }
Struct typetype User struct { Name string `json:"name"` }
Methodfunc (u *User) Rename(n string) { u.Name = n }
Interfacetype Shape interface { Area() float64 }
Generic functionfunc Map[T, U any](xs []T, f func(T) U) []U
Type switchswitch v := x.(type) { case string: }
Start a goroutinego work(ch) or wg.Go(func() { work() })
Channelch := make(chan int, 10)
Wait with a timeoutselect { case v := <-ch: case <-ctx.Done(): }
Clean updefer f.Close()
Sort a sliceslices.Sort(nums)
Format a datet.Format("2006-01-02 15:04")
Encode JSONb, err := json.Marshal(v)
HTTP routemux.HandleFunc("GET /users/{id}", h)
Run the testsgo test -race ./...
Cross compileGOOS=linux GOARCH=arm64 go build ./cmd/app

What Go Is

Go is a statically typed, compiled, garbage collected programming language designed at Google by Robert Griesemer, Rob Pike and Ken Thompson, announced in 2009 and stable since Go 1.0 in 2012. It was built for large codebases and many engineers: fast compilation, one standard format, a small language with few ways to do each thing, and concurrency built into the language through goroutines and channels.

A new major release ships every six months, in February and August, and the two most recent are supported with security fixes. Go 1.27, released on August 19, 2026, is the current version, with Go 1.27.1 from September 1, 2026 as the latest patch release. The Go 1 compatibility promise means code written for Go 1.0 still compiles today.

Go is the language of cloud infrastructure: Docker, Kubernetes, Terraform, Prometheus and etcd are all written in it, which is why it pairs naturally with the Docker cheat sheet and the Kubernetes cheat sheet. It is just as common for back-end development: HTTP APIs, microservices, command line tools and network servers.

Where It Fits

Choose Go for network services, APIs, CLIs and infrastructure tools where you want a single static binary, quick builds, good performance and simple concurrency. It gives up some expressiveness for readability: there are no exceptions, no inheritance and few ways to write the same loop. Rust fits better when you need full control of memory without a garbage collector; Python is quicker for scripts and data work, and the Python cheat sheet covers it.


How a Go Program Is Built and Run

go build hides several steps, and knowing them explains most build errors and deployment questions.

  1. The go command reads go.mod, picks the toolchain and resolves every dependency to an exact version using minimal version selection, verified against go.sum.
  2. It builds packages in dependency order. Each package is compiled as a unit: unused imports and variables, type errors and missing methods are reported here.
  3. The compiler inlines small functions, runs escape analysis to decide what lives on the stack or the heap, and applies a CPU profile when default.pgo is present.
  4. The linker combines the packages and the Go runtime, which contains the scheduler and garbage collector, into one executable, statically linked unless cgo is used.
  5. At run time the scheduler multiplexes goroutines onto GOMAXPROCS operating system threads, and the garbage collector reclaims heap memory concurrently with the program.

Because the result is one file with no runtime to install, deploying Go is copying a binary or building a tiny container image, which the Dockerfile builder above writes for you. It fits any build pipeline with a single go build step.


Go, Rust and Python Compared

The differences that matter when choosing a language for a service or a tool.

QuestionGoRustPython
Memory managementgarbage collectorownership checked at compile timereference counting and GC
Concurrencygoroutines and channels in the languagethreads and async, checked for data racesasyncio, threads limited by the GIL unless free threaded
Error handlingerror values, if err != nilResult and the ? operatorexceptions
Genericstype parameters since 1.18generics and traitstype hints only
Build outputone static binary, fast buildsone binary, slower buildssource plus an interpreter
Learning curvesmall language, quick to learnsteepgentle

Many teams combine them: Go for the services, Python for data and scripts, Rust for the few components where every microsecond or byte counts.


Common Go Mistakes

The same handful of problems shows up in code reviews of most Go projects.

In Code

  • Writing to a nil map, which panics; reading from one is fine.
  • Ignoring an error, or wrapping it with %v instead of %w so errors.Is can no longer find it.
  • Returning a nil pointer of a concrete type as an error, which is not a nil interface.
  • Appending to a sub-slice and overwriting elements the caller still uses.
  • Starting goroutines with no way to stop them, and leaking them when nobody reads their channel.
  • Copying a struct that contains a sync.Mutex, usually through a value receiver.

In Builds and Services

  • Using http.Get or http.ListenAndServe, neither of which sets a timeout.
  • Not closing response bodies, which leaks connections.
  • Never running tests with -race, where data races would show up.
  • Forgetting that a go line below 1.22 keeps the old shared loop variable.
  • Committing a replace directive that points at a local directory.
  • Exposing /debug/pprof on a public port.

How to Learn Go Well

Start With Small Programs and the Standard Library

Take the Tour of Go, then write small command line tools and an HTTP API using only the standard library: net/http, encoding/json, database/sql and log/slog carry you a long way before a framework is needed. Write tests from the start, table driven, with go test -race, which suits a test-driven development habit. Keep your code in version control with the git cheat sheet.

Then Learn Concurrency and the Tools

Learn goroutines, channels and context with small programs you can run under the race detector, then read Effective Go and the standard library source, which is idiomatic Go written by the language's authors. Profile a real service with pprof, deploy it in a container, and wire go vet, a linter and govulncheck into CI with the GitHub Actions builder above. An editor with gopls, set up with the VS Code workflow cheat sheet, shows errors and fixes as you type.


How This Go Cheat Sheet Is Maintained

This page is written and maintained by Bogdan Sandu for TMS Outsource, a software development company. It was checked against the Go 1.25, Go 1.26 and Go 1.27 release notes and the Go 1.27.1 release on September 29, 2026, and the output of the example programs was produced by compiling and running them on Compiler Explorer. If a snippet is wrong or out of date, the date in the byline shows when it was last reviewed.


Go Programming Questions People Actually Ask

The questions developers search for most about Go and about this cheat sheet, answered in a few sentences each.

Is Go the same as Golang?

Yes. The language is called Go; Golang is a nickname that comes from its original website, golang.org, and it stuck because "go" alone is hard to search for. The official site is now go.dev, but both names refer to the same language and toolchain.

What is the latest version of Go?

Go 1.27, released on August 19, 2026, with Go 1.27.1 as the latest patch release. It added generic methods, promoted field names in struct literals, encoding/json/v2 as a standard package, the uuid and crypto/mldsa packages, and a goroutine leak profile. Go 1.27 and Go 1.26 are the two supported releases.

Is Go good for beginners?

Yes. Go has a small language specification, one standard code format, clear compiler errors and a strong standard library, so beginners spend their time on programming rather than on the language. Pointers and explicit error handling take some getting used to, but there are no classes, exceptions or inheritance hierarchies to learn first.

What is the difference between a slice and an array in Go?

An array has a fixed length that is part of its type, such as [3]int, and assigning it copies every element. A slice is a small header that points into an array, with a length and a capacity; it can grow with append, and copies of a slice share the same elements. Most Go code uses slices.

How do goroutines differ from threads?

A goroutine is a function running concurrently, managed by the Go runtime rather than the operating system. It starts with a stack of a few kilobytes that grows as needed, so a program can run hundreds of thousands of them; the scheduler multiplexes them onto a small number of OS threads, set by GOMAXPROCS.

How does error handling work in Go?

Functions that can fail return an error as their last result, and the caller checks it with if err != nil. Add context with fmt.Errorf and the %w verb, and inspect wrapped errors with errors.Is, errors.As or, since Go 1.26, errors.AsType. panic is reserved for bugs and impossible states, not for expected failures.

Does Go have generics?

Yes, since Go 1.18. Functions and types can take type parameters with constraints such as any, comparable and cmp.Ordered, and Go 1.24 added generic type aliases. Go 1.27 added generic methods, so a method can declare its own type parameters, although interface methods still cannot.

What is Go used for?

Mostly for back-end services, APIs, microservices, command line tools, DevOps and cloud infrastructure. Docker, Kubernetes, Terraform, Prometheus, Hugo and CockroachDB are written in Go, and companies use it for high traffic web services because it combines good performance, low memory use and simple deployment as a single binary.

Is this Go cheat sheet up to date?

Yes. It covers Go 1.21 to Go 1.27 and was checked against the Go 1.27.1 release and the release notes of Go 1.25, 1.26 and 1.27 on September 29, 2026. Every snippet that needs a newer release carries a version chip, the version picker hides what your go line cannot build, and the program output was produced by real runs.

Can I download this Go cheat sheet as a PDF?

Yes. The page has a print stylesheet, so your browser's print command with Save as PDF produces a clean copy: the dark theme switches to a print palette, every output and explanation is expanded, and the search box, navigation and footer are left out. The print core button prints only the core snippets for a short reference, and you can collapse sections or run a search first to print only what is still visible.