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.
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.
Install, Run & Build
installing Go, go run and go build, toolchains, a Go toolbox and builders
Builders: go build, Dockerfile, go test, GitHub Actions, Profiling
Install Go
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
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
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
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 ./...
Packages & Imports
package clauses, imports, exported names, init, internal packages and layout
Packages and Imports
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
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
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
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"))
Variables & Constants
var, short declarations, zero values, shadowing, constants and iota
Declaring Variables
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
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
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)
}
Types & Conversions
numeric types, conversions, strconv, overflow, type definitions and aliases
Basic Types
The built-in types
| Type | Size | Notes |
|---|---|---|
bool | 1 byte | true or false |
int, uint | 8 bytes on 64-bit | the default integer type; 4 bytes on 32-bit targets |
int8 to int64 | 1 to 8 bytes | signed, exact width |
uint8 to uint64 | 1 to 8 bytes | unsigned, exact width |
byte | 1 byte | alias for uint8 |
rune | 4 bytes | alias for int32, one Unicode code point |
uintptr | pointer size | integer that can hold an address, for unsafe code |
float32, float64 | 4, 8 bytes | IEEE 754; float64 is the default |
complex64, complex128 | 8, 16 bytes | complex numbers |
string | 16 bytes header | immutable 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
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
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
Control Flow
if, for, range, switch, labels and goto
if and switch
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
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
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()
Strings, Runes & Bytes
UTF-8 strings, the strings package, runes, bytes and building strings
String Basics
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
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
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()
Arrays & Slices
arrays, slices, append, copy, sharing, and the slices package
Arrays and Slices
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
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))
Maps
creating, reading, deleting, iterating in order, sets and the maps package
Map Basics
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
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()
Structs & Embedding
struct types, literals, comparison, embedding, tags and constructors
Structs
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
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
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{}{}))
}
Pointers & Memory
pointers, value and pointer semantics, nil, escape analysis and the garbage collector
Pointers
& 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
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")
}
}
Functions & Closures
multiple returns, variadic functions, closures, defer and function values
Functions
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
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
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())
}
Methods & Interfaces
value and pointer receivers, implicit interfaces, type assertions, type switches and nil interfaces
Methods
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
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)
Generics
type parameters, constraints, generic types, generic methods and inference
Generic Functions
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
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
Rules of thumb
| Use generics for | Prefer instead |
|---|---|
| Containers: sets, stacks, caches, trees | an interface, when behavior differs per type |
| Functions over slices, maps and channels of any element | a plain function, when there is only one type today |
| Removing copies of the same algorithm for int, float64, string | code generation, rarely needed since Go 1.18 |
| Type safe wrappers around any-based APIs | reflection, when the type is only known at run time |
Errors, panic & recover
error values, wrapping, errors.Is, errors.As, AsType, custom errors, Join, panic and recover
Creating and Checking Errors
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
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")
Iterators
range over functions, iter.Seq, iter.Seq2, pull iterators and the iterator helpers
Range over Functions
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
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()
Goroutines & WaitGroup
starting goroutines, waiting for them, GOMAXPROCS and leaks
Starting and Waiting
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
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
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
}
Channels & select
unbuffered and buffered channels, close, range, select, timeouts and deadlocks
Channel Basics
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
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
What each operation does
| Operation | nil channel | open channel | closed channel |
|---|---|---|---|
Send ch <- v | blocks forever | waits for room or a receiver | panics |
Receive <-ch | blocks forever | waits for a value | zero value, ok is false |
close(ch) | panics | closes | panics |
len(ch), cap(ch) | 0 | items in buffer, buffer size | items left, buffer size |
Mutexes & Atomics
sync.Mutex, RWMutex, Once, OnceValue, atomics, sync.Map, sync.Pool and the race detector
Mutexes
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
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
Find data races
go test -race ./... go run -race .
context
cancellation, deadlines, values and causes
Cancellation and Deadlines
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
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
| Do | Do not |
|---|---|
| Pass ctx as the first parameter, named ctx | store a context in a struct field |
| Use r.Context() in HTTP handlers and t.Context() in tests | call context.Background() deep inside library code |
| Call cancel on every path, usually with defer | pass a nil context: use context.TODO() while refactoring |
| Check ctx.Err() or select on ctx.Done() in long loops | use context values for optional function parameters |
Concurrency Patterns
worker pools, pipelines, fan-in, semaphores, rate limits and timeouts
Worker Pool
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
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)
fmt & slog
Printf verbs, width and precision, Stringer, tabwriter, log and structured logging with slog
Printf Verbs
The verbs you use every day
| Verb | Prints | Example output |
|---|---|---|
%v | default format | {ann 31} |
%+v | structs with field names | {Name:ann Age:31} |
%#v | Go syntax | main.User{Name:"ann", Age:31} |
%T | the type | main.User |
%d %b %o %x | integer in base 10, 2, 8, 16 | 42 101010 52 2a |
%f %.2f %e %g | float, fixed, exponent, compact | 3.141593 3.14 3.141593e+00 3.14159 |
%s %q | string, quoted string | gopher "gopher" |
%t | bool | true |
%c %U | rune, Unicode code point | G U+0047 |
%p | pointer address | 0xc000012345 |
%w | wrapped error, fmt.Errorf only | same 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
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
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")
Files, OS & Processes
reading and writing files, bufio, io.Reader, paths, os.Root, embed, flags, environment and commands
Reading and Writing Files
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
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
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))
JSON
Marshal and Unmarshal, struct tags, omitempty and omitzero, streaming, custom marshaling and encoding/json/v2
Marshal and Unmarshal
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
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
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()
net/http
servers with ServeMux patterns, handlers, middleware, JSON APIs, clients and graceful shutdown
Server and Routing
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
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
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"))
Time & Dates
formatting with the reference time, parsing, time zones, durations, Unix times, timers and tickers
Formatting and Parsing
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
| Token | Meaning | Token | Meaning |
|---|---|---|---|
2006 06 | year | 15 03 3 | hour, 24h or 12h |
01 1 Jan January | month | 04 4 | minute |
02 2 _2 | day of month | 05 5 | second |
002 | day of year | .000 .999 | fraction, fixed or trimmed |
Mon Monday | weekday | PM pm | AM/PM marker |
MST | zone name | -0700 -07:00 Z07:00 | zone offset |
Durations and Arithmetic
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
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=+"))
Modules & Workspaces
go.mod, dependencies, versions, replace, workspaces, tool dependencies and private modules
go.mod and Dependencies
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
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
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"))
Testing & Benchmarks
table tests, subtests, helpers, benchmarks with b.Loop, fuzzing, examples, synctest and coverage
Unit Tests
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
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
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)
Tools & Profiling
gofmt, go vet, go fix, linters, govulncheck, pprof, traces, Delve, build tags, cross compiling and PGO
Formatting and Static Checks
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
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
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
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] }))
}
Errors & Gotchas
compiler errors, runtime panics and the surprises that catch Go developers
Compiler Errors
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
| Message | Usual fix |
|---|---|
| declared and not used: x | use it, delete it, or assign to _ while drafting |
| "os" imported and not used | delete the import; goimports does it on save |
| missing return | add a return at the end, or a panic for impossible paths |
| invalid operation: mismatched types | convert explicitly: float64(n) * ratio |
| cannot use x (variable of type T) as U value | convert, or implement the missing method |
| undefined: name | typo, unexported name from another package, or missing import |
| x declared and not used after := in if | the := made a new variable: use = to assign |
| assignment mismatch: 2 variables but f returns 1 value | match the number of results |
Runtime Panics
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
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)
}
Go 1.21 to 1.27
what each release added, the release cycle, support policy and compatibility
What Each Release Added
Language and library highlights
| Release | Date | Highlights |
|---|---|---|
| Go 1.27 | Aug 2026 | generic methods, promoted field names in struct literals, encoding/json/v2, uuid and crypto/mldsa packages, strings.CutLast, goroutineleak profile |
| Go 1.26 | Feb 2026 | new(expr), self-referential generic constraints, Green Tea GC by default, errors.AsType, go fix modernizers, crypto/hpke |
| Go 1.25 | Aug 2025 | sync.WaitGroup.Go, testing/synctest, container-aware GOMAXPROCS, trace flight recorder, http.CrossOriginProtection |
| Go 1.24 | Feb 2025 | generic type aliases, tool directives, testing.B.Loop, os.Root, weak pointers, runtime.AddCleanup, omitzero, Swiss table maps |
| Go 1.23 | Aug 2024 | range over functions, package iter, slices and maps iterator helpers, unique package, timer changes |
| Go 1.22 | Feb 2024 | per-iteration loop variables, range over int, ServeMux method and wildcard patterns, math/rand/v2 |
| Go 1.21 | Aug 2023 | min, 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
Two releases a year, two supported
| Fact | Detail |
|---|---|
| Major releases | every six months, in February and August |
| Supported releases | the two most recent: Go 1.27 and Go 1.26 today |
| Minor releases | security and bug fixes, for example Go 1.27.1 on September 1, 2026 |
| Compatibility | Go 1 programs keep compiling and running; behavior changes go behind GODEBUG settings tied to the go line |
| Next release | Go 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.
- Foundations: installing, running and building Go with the toolbox and builders, packages and imports, variables, constants and iota, types and conversions, and control flow.
- Data: strings, runes and bytes, arrays and slices, maps, structs and embedding, and pointers and memory.
- Functions and types: functions, closures and defer, methods and interfaces, generics, errors, panic and recover, and iterators.
- Concurrency: goroutines and WaitGroup, channels and select, mutexes and atomics, context, and worker pools and pipelines.
- Standard library: fmt and slog, files, the OS and processes, JSON, net/http, and time and dates.
- Tooling: modules and workspaces, testing, benchmarks and fuzzing, and vet, go fix, pprof and Delve.
- Help: compiler errors, panics and gotchas, and what changed from Go 1.21 to Go 1.27.
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.
| Task | Go |
|---|---|
| Declare and assign | name := "gopher" |
| Declare with a type | var count int64 |
| Constant enum | const ( A = iota; B; C ) |
| Function with an error | func load(id int) (*User, error) |
| Check an error | if err != nil { return fmt.Errorf("load: %w", err) } |
| Slice literal | nums := []int{1, 2, 3} |
| Append | nums = append(nums, 4) |
| Map literal | ages := map[string]int{"ann": 31} |
| Map lookup | v, ok := ages["bob"] |
| Loop n times | for i := range n { } |
| Loop over a slice | for i, v := range nums { } |
| Struct type | type User struct { Name string `json:"name"` } |
| Method | func (u *User) Rename(n string) { u.Name = n } |
| Interface | type Shape interface { Area() float64 } |
| Generic function | func Map[T, U any](xs []T, f func(T) U) []U |
| Type switch | switch v := x.(type) { case string: } |
| Start a goroutine | go work(ch) or wg.Go(func() { work() }) |
| Channel | ch := make(chan int, 10) |
| Wait with a timeout | select { case v := <-ch: case <-ctx.Done(): } |
| Clean up | defer f.Close() |
| Sort a slice | slices.Sort(nums) |
| Format a date | t.Format("2006-01-02 15:04") |
| Encode JSON | b, err := json.Marshal(v) |
| HTTP route | mux.HandleFunc("GET /users/{id}", h) |
| Run the tests | go test -race ./... |
| Cross compile | GOOS=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.
- 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.
- 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.
- 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.
- 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.
- 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.
| Question | Go | Rust | Python |
|---|---|---|---|
| Memory management | garbage collector | ownership checked at compile time | reference counting and GC |
| Concurrency | goroutines and channels in the language | threads and async, checked for data races | asyncio, threads limited by the GIL unless free threaded |
| Error handling | error values, if err != nil | Result and the ? operator | exceptions |
| Generics | type parameters since 1.18 | generics and traits | type hints only |
| Build output | one static binary, fast builds | one binary, slower builds | source plus an interpreter |
| Learning curve | small language, quick to learn | steep | gentle |
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.