Compiled and systems languages cheat sheetGo logo

Go cheat sheet

Go syntax on one page, grouped by what you are trying to do: slices, maps, structs, interfaces, errors, goroutines and channels, checked on Go 1.27.

Last updated

Go is a small, statically typed language from Google that compiles to a single native binary, starts in milliseconds and has concurrency built in. It runs Docker, Kubernetes, Terraform and a great many web services. The reference below is grouped by what you are trying to do, and the filter box searches all of it at once. Type slices and every slice helper comes to you, or type Go 1.27 to see what that release added.

Every snippet is checked against Go 1.27, the current release. Go 1.22 is the baseline, so anything newer says so in the notes column and you can tell at a glance whether it will build with the Go you have. Names like nums, people and Person are placeholders for your own. If you do not have Go installed, the official Docker image is the quickest way to try something: docker run --rm -it -v "$PWD":/src -w /src golang:1.27 bash, and the Docker cheat sheet has the rest. Coming from C? The C cheat sheet is laid out the same way, and Go keeps its syntax and pointers while dropping pointer arithmetic, header files and free.

The go command

TaskCommandNotes
Check which version you havego version
Start a modulego mod init example.com/helloWrites go.mod. The path is what other code imports it by
Run the program in this foldergo run .Compiles to a temporary folder, runs it, and cleans up
Pass arguments to your programgo run . -port 8080Everything after the package goes to os.Args
Build a binarygo build go build -o bin/app .Without -o the binary is named after the module or folder
Build for another OSGOOS=linux GOARCH=arm64 go build -o app-linux .No extra toolchain needed. go tool dist list shows every pair
Build with no C dependenciesCGO_ENABLED=0 go build .A fully static binary that runs in a scratch Docker image
Smaller binarygo build -ldflags="-s -w" .Drops the symbol table and debug info
Set a variable at build timego build -ldflags="-X main.version=1.2.3" .version must be a package-level string var
Add or upgrade a dependencygo get github.com/google/go-cmp@latestUpdates go.mod and go.sum. @v0.7.0 pins a version
Upgrade every dependencygo get -u ./...-u=patch for bug-fix releases only
Add missing and drop unusedgo mod tidyRun it before every commit that touches imports
Why is this dependency herego mod why -m golang.org/x/text
List dependencies, and upgradesgo list -m all go list -m -u all
Use a local copy of a dependencygo mod edit -replace example.com/lib=../libAdds a replace line to go.mod. -dropreplace undoes it
Work on several modules togethergo work init ./api ./sharedWrites go.work, which wins over the replace lines
Install a command-line programgo install golang.org/x/tools/cmd/stringer@latestLands in $(go env GOPATH)/bin, usually ~/go/bin
Track a tool in go.modgo get -tool golang.org/x/tools/cmd/stringerGo 1.24. Then go tool stringer runs that exact version
Format the codego fmt ./... gofmt -l .gofmt -l lists files that are not formatted. There is no style debate
Catch likely bugsgo vet ./...Printf mistakes, unkeyed struct fields, copied locks and more
Update code to newer idiomsgo fix -diff ./...Go 1.26. Drop -diff to apply them, for example min and max over if statements
Run the testsgo test ./..../... means this folder and every one below it
Run some of the testsgo test -run 'TestParse/empty' ./parserA regex, with / to pick a subtest
Verbose, and skip the cachego test -v -count=1 ./...Passing results are cached until the code changes
Find data racesgo test -race ./... go run -race .Slower, so it is for tests and debugging
Test coveragego test -coverprofile=cover.out ./... go tool cover -html=cover.out-cover alone prints a percentage
Benchmarksgo test -bench=. -benchmemThe tests run too, unless you add -run='^$'
Read the docs for somethinggo doc strings.Cut go doc -all net/httpOffline. pkg.go.dev has the same docs on the web
See the build settingsgo env GOPATH GOOS GOARCHgo env -w GOPRIVATE=example.com/* saves one
Run go:generate linesgo generate ./...Not part of go build. You run it and commit the output
Clear the cachesgo clean -cache -testcache go clean -modcache

go run and go build take packages, not files: pass . for this folder. go run main.go works for a single-file program, but as soon as there is a second file in the package it fails with undefined names, because only the file you listed was compiled.

Packages and modules

A package is a folder of .go files that all start with the same package line. A module is a tree of packages with a go.mod at the top, and it is the unit you version and download. Anything whose name starts with a capital letter is exported; everything else is private to its package.

TaskCodeNotes
Make a programpackage main func main() { fmt.Println("hi") }package main plus func main is a program. Any other name is a library
Import one packageimport "fmt"
Import severalimport ("fmt"; "os")gofmt puts one per line. Unused imports do not compile
Rename an importimport str "strings"Or to avoid a clash between two packages called rand
Import only for its side effectsimport _ "image/png"Runs its init functions. This one registers the PNG decoder
Import a package from your moduleimport "example.com/shop/store"The module path plus the folder
Exported or notfunc Parse() {} func parse() {}Capitalised means visible to other packages. There is no public keyword
Run setup when the package loadsfunc init() { }Before main, once per package. Keep it small
What go.mod holdsmodule example.com/shop go 1.22 require github.com/google/go-cmp v0.7.0The go line is the oldest Go the module needs, and sets the language version
Code only your module can importexample.com/shop/internal/storeAnything under internal/ is importable only from inside its parent folder
Bundle files into the binary//go:embed static var static embed.FSAlso into a string or []byte for one file. Needs import embed
Build a file only on one OS//go:build linuxFirst line of the file. Or name the file store_linux.go
Tests live beside the codestore.go store_test.goFiles ending in _test.go only build under go test

Types, variables and constants

Go is statically typed with very little ceremony. := declares and infers in one step, every variable starts at its type's zero value, and converting between types is always explicit, even from int to int64.

TaskCodeNotes
Declare and infer the typename := "Ada"Inside functions only
Declare with a typevar count intStarts at the zero value: 0, "", false or nil. Nothing is ever uninitialised
Several at oncea, b := 1, "two" var x, y float64:= needs at least one new name on the left
Package-level variablevar debug = false:= does not work outside a function
Constantconst MaxUsers = 100Numbers, strings, bools and runes. Untyped until used
Numbered constantsconst ( Small = iota; Medium; Large )iota is 0, 1, 2 within one const block
An enum with its own typetype Size int const ( Small Size = iota; Medium; Large )Go has no enum keyword. This is the idiom
Integersint int64 int32 uint8 uintint is 64 bits on 64-bit platforms. byte is uint8, rune is int32
Floating pointf := 3.14float64 unless you say float32
Boolean and stringok := true s := "text"
Number literals1_000_000 0xFF 0o755 0b1010The _ is a digit separator
Convert between number typesfloat64(n) int(f) int64(n)Always explicit. int(f) truncates, so 3.9 becomes 3
Number to stringstrconv.Itoa(42)Not string(42), which gives "*", the character with code 42
String to numbern, err := strconv.Atoi("42")Returns an error for bad input instead of panicking
Other parsesstrconv.ParseFloat("2.5", 64) strconv.ParseBool("true")
Largest and smallest valuesmath.MaxInt math.MaxInt64 math.MinInt32Integers wrap silently on overflow
Pointer to a variablep := &x *p = 10No pointer arithmetic. p.Field works without ->
Pointer to a new zero valuep := new(int)Same as var v int; p := &v
Pointer to a value in one stepp := new(42)Go 1.26. Handy for optional fields like *int in structs
A new type from an old onetype Celsius float64A distinct type: you cannot add a Celsius to a float64 without converting
Another name for a typetype Text = stringAn alias. Text and string are interchangeable
Generic aliastype Set[T comparable] = map[T]struct{}Go 1.24
Any type at allvar v any = 42any is short for interface{}
Smallest and largest of some valuesmin(a, b, c) max(x, y)Built in. Work on numbers and strings
Print a value's typefmt.Printf("%T\n", x)

Strings, runes and fmt

A string is a read-only sequence of bytes, normally UTF-8. len and indexing count bytes, not characters. A rune is one Unicode code point, and ranging over a string gives you runes.

TaskCodeNotes
String with escapes"tab\there\n"Double quotes: \n, \t, \" and \u00e9 work
Raw string`C:\temp\notes.txt`Backticks: no escapes, and it can span lines. Good for regexes and JSON
Join two stringss := first + " " + last
Length in bytes, and in characterslen("héllo") utf8.RuneCountInString("héllo")6 and 5. unicode/utf8
One bytes[0]A byte, not a character. s[0] = 'x' does not compile
Loop over the charactersfor i, r := range s { }r is a rune and i is its byte offset
Part of a strings[1:4] s[:3] s[2:]Byte positions. Start included, end excluded
Contains, prefix, suffixstrings.Contains(s, "cat") strings.HasPrefix(s, "/") strings.HasSuffix(s, ".go")
Find textstrings.Index(s, "cat")-1 if it is not there
Split and joinstrings.Split("a,b,c", ",") strings.Join(parts, ", ")
Split on any whitespacestrings.Fields(s)Drops empty pieces, unlike Split
Split around the first matchkey, value, ok := strings.Cut("port=8080", "=")
Split around the last matchdir, file, ok := strings.CutLast("a/b/c.txt", "/")Go 1.27. dir is a/b, file is c.txt
Replacestrings.ReplaceAll(s, "cat", "dog") strings.Replace(s, "cat", "dog", 1)The 1 is how many to replace
Trimstrings.TrimSpace(s) strings.TrimPrefix(s, "v") strings.TrimSuffix(s, ".go")
Change casestrings.ToUpper(s) strings.ToLower(s)
Compare ignoring casestrings.EqualFold(a, b)
Comparea == b a < b== compares the text. < is byte order
Repeatstrings.Repeat("-", 20)
Build one up in a loopvar sb strings.Builder sb.WriteString(word) s := sb.String()+= in a loop copies the whole string every time
Loop over the linesfor line := range strings.Lines(text) { }Go 1.24. Each line keeps its trailing newline
Loop over the pieces without a slicefor part := range strings.SplitSeq(s, ",") { }Go 1.24. Also strings.FieldsSeq
To and from bytes and runes[]byte(s) string(b) []rune(s)Each one copies
Test a characterunicode.IsDigit(r) unicode.IsLetter(r) unicode.IsSpace(r)
Print with spaces and a newlinefmt.Println("total:", total)
Print with a formatfmt.Printf("%s is %d\n", name, age)Printf adds no newline
Format into a strings := fmt.Sprintf("%.2f", price)
The formatting verbs%v %+v %#v %T %d %s %q %x %t %pAny value, with field names, as Go syntax, its type, int, string, quoted, hex, bool, pointer
Width and precision"%-10s|%5d|%8.2f"- aligns left. %8.2f is 8 wide with 2 decimals
Print an errorfmt.Fprintln(os.Stderr, "failed:", err)

Arrays and slices

An array has a fixed length that is part of its type, so you rarely use one directly. A slice is a view of an array with a length and a capacity, and it is what every Go function takes and returns. The slices package has the everyday helpers.

TaskCodeNotes
Arrayvar a [3]int b := [...]int{1, 2, 3}[3]int and [4]int are different types. Arrays copy on assignment
Slice with valuesnums := []int{3, 1, 2}
Empty slice with room to grows := make([]int, 0, 100)Length 0, capacity 100. Saves re-allocating in a loop
Slice of zeross := make([]int, 5)Length 5. append adds after the zeros, not over them
Add to the endnums = append(nums, 4, 5)Always assign the result: append may return a new array
Add another slicea = append(a, b...)
Length and capacitylen(nums) cap(nums)
Part of a slicenums[1:3] nums[:2] nums[2:]Shares the array with nums, so writes show through
Part, with no room to grownums[1:3:3]The third number caps capacity, so a later append copies instead of overwriting nums
Loopfor i, v := range nums { }v is a copy. Write through nums[i] to change the slice
Copyc := slices.Clone(nums)Or copy(dst, src), which copies min(len(dst), len(src)) items
Combine severalall := slices.Concat(a, b, c)
Contains and findslices.Contains(nums, 3) slices.Index(nums, 3)Index gives -1 if it is not there
Find with a conditioni := slices.IndexFunc(people, func(p Person) bool { return p.Age > 30 })
Sortslices.Sort(nums)In place. Numbers, strings, anything ordered
Sort by a fieldslices.SortFunc(people, func(a, b Person) int { return cmp.Compare(a.Age, b.Age) })Negative, zero or positive. SortStableFunc keeps ties in order
Sort by two fieldscmp.Or(cmp.Compare(a.Age, b.Age), strings.Compare(a.Name, b.Name))cmp.Or returns the first result that is not zero
Search a sorted slicei, found := slices.BinarySearch(sorted, 7)
Smallest, largest, reverseslices.Min(nums) slices.Max(nums) slices.Reverse(nums)Min and Max panic on an empty slice
Equalslices.Equal(a, b)== only compares a slice with nil
Insert and deletenums = slices.Insert(nums, 1, 99) nums = slices.Delete(nums, 1, 2)Delete removes items 1 up to, not including, 2
Remove every matchnums = slices.DeleteFunc(nums, func(n int) bool { return n < 0 })
Drop repeats in a sorted slicenums = slices.Compact(nums)
Batches of threefor batch := range slices.Chunk(nums, 3) { }Go 1.23
Collect an iterator into a slicekeys := slices.Collect(maps.Keys(m))Go 1.23
Gridgrid := make([][]int, rows) for i := range grid { grid[i] = make([]int, cols) }A slice of slices. Each row is made separately
Set every item to zeroclear(nums)Keeps the length

A nil slice (var s []int) has length 0 and works with len, range and append, so there is no need to make an empty one first. The one place it differs is JSON, where a nil slice encodes as null and an empty one as [].

Maps

A map is Go's hash table. Keys can be any comparable type (numbers, strings, structs of those, pointers), but not slices, maps or functions. Reading a missing key is not an error: it gives the zero value.

TaskCodeNotes
Map with valuesages := map[string]int{"Ada": 36, "Linus": 29}
Empty mapm := make(map[string]int)Not var m map[string]int, which is nil and panics on write
Add or replaceages["Bob"] = 40
Look upages["Zoe"]0 for a missing key, with no error
Look up and check it was thereage, ok := ages["Zoe"]The comma ok idiom. ok is false for a missing key
Only if it is thereif age, ok := ages["Ada"]; ok { }
Deletedelete(ages, "Ada")Does nothing if the key is missing
How manylen(ages)
Loopfor name, age := range ages { }The order is random on purpose, and changes from run to run
Loop in key orderfor _, name := range slices.Sorted(maps.Keys(ages)) { }Go 1.23
Count occurrencescounts[word]++A missing key starts at 0
Group into listsbyCity[p.City] = append(byCity[p.City], p)A missing key gives a nil slice, which append accepts
A setseen := map[string]bool{} seen[x] = true if seen[x] { }Or map[string]struct{} to store nothing per key
Struct as the keytype Point struct{ X, Y int } grid := map[Point]string{}Every field must be comparable
Copyc := maps.Clone(ages) maps.Copy(dst, src)
Equalmaps.Equal(a, b)
Delete every matchmaps.DeleteFunc(ages, func(k string, v int) bool { return v < 18 })
Remove everythingclear(ages)

Maps are not safe for concurrent use. Two goroutines writing to one map, or one writing while another reads, stops the program with fatal error: concurrent map writes. Guard it with a sync.Mutex, or use sync.Map for a cache that is written once and read many times.

Control flow

No brackets round conditions, braces always required, and for is the only loop.

TaskCodeNotes
if, else if, elseif n > 0 { } else if n < 0 { } else { }The condition must be a bool. if n { } does not compile
if with a setup statementif err := save(); err != nil { }err exists only inside the if and its else
Pick one of two valueslabel := "odd" if n%2 == 0 { label = "even" }Go has no ?: operator
Counting loopfor i := 0; i < 10; i++ { }
Count to nfor i := range 10 { }0 to 9
Loop over a slice, map, string or channelfor i, v := range items { }for _, v to skip the index, for i := range for just the index
while loopfor n > 1 { }
Loop foreverfor { }Leave with break or return
Skip to the next round, or stopcontinue break
Leave nested loopsouter: for _, row := range grid { for _, v := range row { if v < 0 { break outer } } }A labelled break. continue outer works too
switchswitch day { case "sat", "sun": rest() default: work() }No fall-through and no break needed. A case can list several values
switch with conditionsswitch { case n < 0: s = "negative"; case n == 0: s = "zero"; default: s = "positive" }The tidy way to write a long if-else chain
Fall into the next casefallthroughThe last statement of a case. Rarely needed
Run something when the function returnsdefer f.Close()Runs however the function ends, even on a panic. Several run last in, first out
Loop over an iteratorfor k, v := range maps.All(m) { }Go 1.23. Any func(yield func(K, V) bool) works in a range

Functions and multiple returns

A function can return several values, and a result plus an error is the usual shape. Everything is passed by value, so pass a pointer when a function should change the caller's variable. Functions are values: store them, pass them, return them.

TaskCodeNotes
Definefunc add(a, b int) int { return a + b }Parameters of the same type can share it
Return two valuesfunc divmod(a, b int) (int, int) { return a / b, a % b }
Take bothq, r := divmod(7, 2)3 and 1
Ignore oneq, _ := divmod(7, 2)_ throws a value away. An unused variable does not compile
Return a value or an errorfunc load(path string) ([]byte, error)The error goes last
Named resultsfunc minMax(nums []int) (lo, hi int) { lo, hi = slices.Min(nums), slices.Max(nums); return }A bare return returns them. Best kept for short functions
Any number of argumentsfunc sum(nums ...int) intnums is a []int inside
Pass a slice to itsum(1, 2, 3) sum(nums...)
Change the caller's variablefunc reset(n *int) { *n = 0 } reset(&count)
Function in a variablesquare := func(x int) int { return x * x }
Function as a parameterfunc apply(f func(int) int, x int) int { return f(x) }
Closure that keeps statefunc counter() func() int { n := 0; return func() int { n++; return n } }Each call to counter gets its own n
Call a function literal at oncefunc() { fmt.Println("now") }()Common with defer and go
Generic functionfunc Map[T, U any](s []T, f func(T) U) []UType arguments are usually inferred: Map(nums, strconv.Itoa)
Only ordered typesfunc Largest[T cmp.Ordered](s []T) TNumbers and strings: anything < works on
Your own constrainttype Number interface { ~int | ~int64 | ~float64 }~int also accepts types defined on int, like type Celsius int
Write your own iteratorfunc Countdown(n int) iter.Seq[int] { return func(yield func(int) bool) { for i := n; i > 0; i-- { if !yield(i) { return } } } }Go 1.23. Loop over it with for i := range Countdown(3)

Structs and methods

Go has no classes. A struct holds the data, methods attach to any named type, and embedding one struct in another shares its fields and methods. That is composition, not inheritance: there is no base class to override.

TaskCodeNotes
Define a structtype Point struct { X, Y int }
Create onep := Point{X: 1, Y: 2}Fields you leave out get their zero value
Create one by positionPoint{1, 2}Breaks when a field is added. go vet flags it for other packages' types
Create a pointer to onep := &Point{X: 1}p.X works on a pointer too
Read and change a fieldp.X p.X = 10
One-off structcfg := struct { Host string; Port int }{"localhost", 8080}An anonymous struct. Handy in tests and for JSON
Comparea == bWorks when every field is comparable
Methodfunc (p Point) Dist() float64 { return math.Hypot(float64(p.X), float64(p.Y)) }p is the receiver, and a copy
Method that changes the structfunc (p *Point) Move(dx, dy int) { p.X += dx; p.Y += dy }A pointer receiver. Go takes the address for you: p.Move(1, 1)
Constructorfunc NewServer(addr string) *Server { return &Server{addr: addr, timeout: 30 * time.Second} }Just a function. NewX is the convention
Embed a structtype Admin struct { User; Level int }Admin gets User's fields and methods: a.Name, a.Greet()
Reach the embedded onea.User.NameAlways works, even when the short form is ambiguous
Set a promoted field in a literalAdmin{Name: "Ada", Level: 2}Go 1.27. Before it, Admin{User: User{Name: "Ada"}, Level: 2}
Methods on any named typetype Celsius float64 func (c Celsius) F() float64 { return float64(c)*9/5 + 32 }
Custom text for printingfunc (p Point) String() string { return fmt.Sprintf("(%d, %d)", p.X, p.Y) }fmt uses it for %v and Println. This is the Stringer interface
Print with field namesfmt.Printf("%+v\n", p){X:1 Y:2}
Struct tagsName string `json:"name,omitempty"`Read by packages such as encoding/json. omitempty skips 0, "", nil and empty slices
Leave out zero structs in JSONCreated time.Time `json:"created,omitzero"`Go 1.24. omitempty never skips a struct

Value or pointer receiver? Use a pointer when the method changes the struct, when the struct is large, or when it holds something that must not be copied such as a sync.Mutex. Otherwise a value receiver is fine. Pick one kind for all of a type's methods, because it decides which of T and *T satisfies an interface.

Interfaces

An interface is a set of methods, and any type that has those methods satisfies it. There is no implements keyword, so a type can satisfy an interface written years later in another package. Keep interfaces small: one or two methods is normal.

TaskCodeNotes
Definetype Shape interface { Area() float64 }
Satisfy itfunc (c Circle) Area() float64 { return math.Pi * c.R * c.R }That is all. Circle is now a Shape
Accept any Shapefunc Total(shapes ...Shape) float64
Check at compile timevar _ Shape = (*Square)(nil)Fails to build if *Square stops satisfying Shape
Get the concrete type backc, ok := s.(Circle)A type assertion. Without ok it panics on the wrong type
Branch on the typeswitch v := x.(type) { case int: fmt.Println(v + 1); case string: fmt.Println(len(v)); default: fmt.Println("other") }A type switch. v has the case's type in each branch
Check for an optional methodif s, ok := v.(fmt.Stringer); ok { fmt.Println(s.String()) }
Combine interfacestype ReadWriter interface { io.Reader; io.Writer }
Accept anythingfunc Log(v any)Then a type switch to do something with it
The ones you meet everywhereerror fmt.Stringer io.Reader io.Writer http.Handler
Methods with pointer receiversvar s Shape = &Square{Side: 2}If Area is on *Square, only *Square is a Shape, not Square

Accept interfaces, return concrete types: a function that takes an io.Reader works with files, network connections, strings and buffers alike, and one that returns *os.File lets the caller use everything a file can do. Define an interface where it is used, not beside the type that satisfies it.

Generics

TaskCodeNotes
Generic typetype Stack[T any] struct { items []T }
Method on itfunc (s *Stack[T]) Push(v T) { s.items = append(s.items, v) }
Use itvar s Stack[string] s.Push("a")
The zero value of Tvar zero TFor returning nothing from a generic function
Keys you can compare with ==func Unique[T comparable](s []T) []Tcomparable is what map keys need
Several type parameterstype Pair[K comparable, V any] struct { Key K; Value V }
Method with its own type parameterfunc (s *Stack[T]) Map[U any](f func(T) U) []UGo 1.27. Called as s.Map(strings.ToUpper), with U inferred
Say the types yourselfMap[int, string](nums, strconv.Itoa)When inference cannot work them out

Errors

Go has no exceptions for ordinary failures. A function that can fail returns an error as its last value, and the caller checks it straight away. Wrap an error with context as it travels up, and check for particular errors with errors.Is and errors.As rather than by comparing the text.

TaskCodeNotes
Check an errordata, err := os.ReadFile(path) if err != nil { return err }
Make oneerrors.New("name is empty")
Make one with valuesfmt.Errorf("port %d is out of range", port)
Add context and keep the causereturn fmt.Errorf("load %s: %w", path, err)%w wraps it, so errors.Is and errors.As still find the cause
An error callers can check forvar ErrNotFound = errors.New("not found")A sentinel error. Name it ErrSomething
Is it that error, at any depthif errors.Is(err, fs.ErrNotExist) { }Looks through every wrapped layer. == only checks the outer one
Your own error typetype ValidationError struct { Field string } func (e *ValidationError) Error() string { return e.Field + " is invalid" }Anything with Error() string is an error
Is it that type, and get itvar ve *ValidationError if errors.As(err, &ve) { fmt.Println(ve.Field) }
The same, in one lineif ve, ok := errors.AsType[*ValidationError](err); ok { }Go 1.26
Several errors as oneerr := errors.Join(err1, err2)nil if all of them are nil. Is and As check each one
Stop the program: a bugpanic("unreachable")For things that should never happen, not for bad input
Recover from a panicdefer func() { if r := recover(); r != nil { log.Println("recovered:", r) } }()Only works inside a deferred function
Print and exitlog.Fatal(err)Exits with status 1, and deferred calls do not run
Exit with a statusos.Exit(2)Also skips deferred calls
Handle a Close error on a file you wrotedefer func() { err = errors.Join(err, f.Close()) }()Needs a named err result. A failed Close can mean lost data

Goroutines and channels

A goroutine is a function running concurrently, and it is cheap enough to start thousands. A channel is a typed pipe between goroutines: a send waits until someone receives, unless the channel has buffer space. When main returns, the program ends without waiting for other goroutines.

TaskCodeNotes
Start a goroutinego fetch(url)Returns at once. Any return value is thrown away
Start several, wait for allvar wg sync.WaitGroup for _, u := range urls { wg.Go(func() { fetch(u) }) } wg.Wait()Go 1.25. Each loop round has its own u
The same before Go 1.25wg.Add(1) go func() { defer wg.Done(); fetch(u) }()Call Add before go, not inside the goroutine
Make a channelch := make(chan int)Unbuffered: a send waits for a receiver
Channel with a bufferch := make(chan int, 10)Sends only wait when 10 are waiting to be read
Send and receivech <- 42 v := <-ch
Close itclose(ch)The sender closes, never the receiver. Sending on a closed channel panics
Receive until it is closedfor v := range ch { }Ends when ch is closed and empty
Was it closedv, ok := <-chok is false once ch is closed and empty
Send-only and receive-onlyfunc produce(out chan<- int) func consume(in <-chan int)The compiler stops the wrong direction
Protect shared datavar mu sync.Mutex mu.Lock() defer mu.Unlock()Put the mutex in the struct beside what it guards
Many readers, one writervar mu sync.RWMutex mu.RLock() defer mu.RUnlock()
A counter without a lockvar hits atomic.Int64 hits.Add(1) hits.Load()sync/atomic
Run something once, lazilyvar config = sync.OnceValue(loadConfig) cfg := config()loadConfig runs on the first call only, even from many goroutines
At most four at a timesem := make(chan struct{}, 4) sem <- struct{}{} defer func() { <-sem }()A buffered channel as a semaphore
Run several and stop at the first errorg, ctx := errgroup.WithContext(ctx) g.Go(func() error { return fetch(ctx, u) }) err := g.Wait()golang.org/x/sync/errgroup. The first error cancels ctx
How many run in parallelruntime.GOMAXPROCS(0)One per CPU core by default. From Go 1.25, on Linux, it also respects a container's CPU limit

Share memory by communicating: hand data from goroutine to goroutine over channels, so only one of them owns it at a time. Reach for a mutex when several goroutines genuinely share a value, such as a cache or a counter. Run your tests with -race either way.

select, timeouts and context

select waits on several channel operations at once and runs whichever is ready first. A context.Context carries a deadline and a cancel signal through a call chain, so a slow request or a Ctrl-C can stop every goroutine working on it.

TaskCodeNotes
Wait on several channelsselect { case msg := <-inbox: handle(msg); case err := <-errs: return err }If several are ready, it picks one at random
Give up after a timeselect { case res := <-results: use(res); case <-time.After(2 * time.Second): return errors.New("timed out") }
Do not wait at allselect { case ch <- v: default: dropped++ }default runs when nothing else is ready
Stop when told tocase <-done: returnClose done to tell every goroutine watching it at once
Do something every secondt := time.NewTicker(time.Second) defer t.Stop() for range t.C { }
Switch a case offinbox = nilA nil channel is never ready, so select skips that case
Cancel after a timectx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel()Always call cancel, or the timer leaks until it fires
Cancel by handctx, cancel := context.WithCancel(context.Background())
Stop when the context endscase <-ctx.Done(): return ctx.Err()context.Canceled or context.DeadlineExceeded
Take a contextfunc Fetch(ctx context.Context, url string) errorThe first parameter, named ctx, by convention
HTTP request that can be cancelledreq, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
Cancel on Ctrl-Cctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) defer stop()os/signal. Pass ctx to everything that should stop

Testing

Tests are ordinary Go in files ending _test.go, run by go test. There is no assertion library in the standard library: compare, and call t.Errorf with what you got and what you wanted.

TaskCodeNotes
A testfunc TestAdd(t *testing.T) { if got := Add(2, 3); got != 5 { t.Errorf("Add(2, 3) = %d, want 5", got) } }Name starts with Test. Errorf records a failure and carries on
Fail and stop this testt.Fatalf("open: %v", err)
Table-driven testsfor _, tc := range tests { t.Run(tc.name, func(t *testing.T) { }) }tests is a slice of structs, one per case. Each gets its own name
Run cases in parallelt.Parallel()
Report failures at the caller's linet.Helper()First line of a helper function
Temporary folderdir := t.TempDir()Deleted when the test ends
Clean up afterwardst.Cleanup(func() { db.Close() })
A context for the testctx := t.Context()Go 1.24. Cancelled just before Cleanup functions run
Skipt.Skip("needs a database")Or if testing.Short() { t.Skip() } with go test -short
Benchmarkfunc BenchmarkAdd(b *testing.B) { for b.Loop() { Add(2, 3) } }Go 1.24. Before it, for range b.N
Example that doubles as a testfunc ExampleAdd() { fmt.Println(Add(2, 3)) }End the body with a // Output: 5 comment. Shows in go doc, and fails if the output differs
Fuzz testfunc FuzzParse(f *testing.F) { f.Add("1,2"); f.Fuzz(func(t *testing.T, s string) { Parse(s) }) }go test -fuzz=FuzzParse feeds it random input
Test an HTTP handlerrec := httptest.NewRecorder() handler(rec, httptest.NewRequest("GET", "/users/1", nil))rec.Code and rec.Body hold the response
A real server for a testsrv := httptest.NewTestServer(t, handler)Go 1.27. In memory, and closed when the test ends. httptest.NewServer before that

Files, JSON, time and HTTP

TaskCodeNotes
Read a whole filedata, err := os.ReadFile("notes.txt")A []byte. string(data) for text
Write a fileerr := os.WriteFile("out.txt", data, 0o644)Replaces it. 0o644 is the permission for a new file
Read line by linesc := bufio.NewScanner(f) for sc.Scan() { line := sc.Text() } err := sc.Err()f from os.Open. Lines over 64 KB need sc.Buffer
Open and closef, err := os.Open(path) if err != nil { return err } defer f.Close()Read only. os.Create makes or truncates a file for writing
Append to a filef, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
Does it existif _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) { }
Make foldersos.MkdirAll("out/logs", 0o755)Fine if they already exist
List a folderentries, err := os.ReadDir(".")e.Name() and e.IsDir() on each entry
Build a pathfilepath.Join("data", "users.json")The right separator for the OS
Command-line argumentsos.Args[1:]os.Args[0] is the program
Command-line flagsport := flag.Int("port", 8080, "port to listen on") flag.Parse()*port for the value. -h prints the help for you
Environment variableos.Getenv("HOME") v, ok := os.LookupEnv("TOKEN")Getenv gives "" when it is not set
Struct to JSONdata, err := json.Marshal(user)Only exported fields. MarshalIndent for pretty output
JSON to a structvar u User err := json.Unmarshal(data, &u)Pass a pointer. Unknown fields are ignored
JSON straight from a streamerr := json.NewDecoder(resp.Body).Decode(&u)
The newer JSON packageimport "encoding/json/v2"Go 1.27. Stricter and faster, with the same Marshal and Unmarshal. encoding/json stays
Now, and time sincestart := time.Now() elapsed := time.Since(start)
Pausetime.Sleep(500 * time.Millisecond)
A length of timed := 90 * time.Minute d.Hours()1.5. Durations print as 1h30m0s
Date arithmetict.Add(24 * time.Hour) t.AddDate(0, 1, 0) t2.Sub(t1)
Format a timet.Format("2006-01-02 15:04") t.Format(time.RFC3339)The layout is the reference time Mon Jan 2 15:04:05 2006, not YYYY-MM-DD
Parse a timet, err := time.Parse(time.DateOnly, "2026-09-27")time.DateOnly is "2006-01-02"
HTTP GETresp, err := http.Get(url) if err != nil { return err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body)Check err before the defer: resp is nil on error. A 404 is not an error, so check resp.StatusCode
Client with a timeoutclient := &http.Client{Timeout: 10 * time.Second}http.Get and the default client never time out
Web server with routeshttp.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, r.PathValue("id")) })Method and path wildcards in the pattern
Start itlog.Fatal(http.ListenAndServe(":8080", nil))
Send JSON backw.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(v)
Structured loggingslog.Info("started", "port", 8080)log/slog. slog.New(slog.NewJSONHandler(os.Stdout, nil)) for JSON lines
Random numberrand.IntN(6) + 1math/rand/v2. 1 to 6. crypto/rand for anything secret
Unique IDid := uuid.New() uuid.NewV7()Go 1.27. Package uuid. Version 7 sorts by creation time
Match a regexre := regexp.MustCompile(`^(\w+)@(\w+)\.com$`) m := re.FindStringSubmatch(email)A raw string saves escaping. m[1] is the first group, nil if no match
Replace with a regexre.ReplaceAllString(s, " ")
Run another programout, err := exec.Command("git", "status", "--short").Output()os/exec. No shell, so no pipes or globbing

Counting words, start to finish

A map to count, a slice of the keys, and slices.SortFunc with cmp.Or to sort by two keys. It prints the three most common words, ties broken alphabetically. Save it as main.go in a folder with a module (go mod init words) and run it with go run ..

package main
 
import (
	"cmp"
	"fmt"
	"maps"
	"slices"
	"strings"
)
 
func main() {
	text := "the cat sat on the mat and the cat slept"
 
	counts := map[string]int{}
	for _, word := range strings.Fields(text) {
		counts[word]++
	}
 
	words := slices.Collect(maps.Keys(counts))
	slices.SortFunc(words, func(a, b string) int {
		return cmp.Or(
			cmp.Compare(counts[b], counts[a]), // most frequent first
			strings.Compare(a, b),             // then A to Z
		)
	})
 
	for _, word := range words[:3] {
		fmt.Printf("%-5s %d\n", word, counts[word])
	}
	// the   3
	// cat   2
	// and   1
}

slices.Collect(maps.Keys(...)) needs Go 1.23. On Go 1.22, build the slice with a loop: for word := range counts { words = append(words, word) }.

slices.SortFunc is not a stable sort, which is why the comparison breaks ties itself. It is pattern-defeating quicksort: a quick sort that switches to heap sort when it spots a bad run of pivots and finishes small ranges with insertion sort. All three are on the site as step-through visualisations.

Interfaces and a type switch

Shape is satisfied by any type with an Area() float64 method, with no declaration saying so. Total works on all of them, and describe uses a type switch to get the concrete type back.

package main
 
import (
	"fmt"
	"math"
)
 
type Shape interface {
	Area() float64
}
 
type Circle struct{ R float64 }
type Rect struct{ W, H float64 }
 
func (c Circle) Area() float64 { return math.Pi * c.R * c.R }
func (r Rect) Area() float64   { return r.W * r.H }
 
// String makes Rect a fmt.Stringer, so Println and %v use it.
func (r Rect) String() string { return fmt.Sprintf("%gx%g rectangle", r.W, r.H) }
 
func Total(shapes ...Shape) float64 {
	sum := 0.0
	for _, s := range shapes {
		sum += s.Area()
	}
	return sum
}
 
func describe(s Shape) string {
	switch v := s.(type) {
	case Circle:
		return fmt.Sprintf("circle of radius %g", v.R)
	case fmt.Stringer:
		return v.String()
	default:
		return fmt.Sprintf("%T", v)
	}
}
 
func main() {
	shapes := []Shape{Circle{R: 1}, Rect{W: 3, H: 4}}
	for _, s := range shapes {
		fmt.Printf("%-18s %6.2f\n", describe(s), s.Area())
	}
	fmt.Printf("%-18s %6.2f\n", "total", Total(shapes...))
	// circle of radius 1   3.14
	// 3x4 rectangle       12.00
	// total               15.14
}

A case in a type switch can name an interface as well as a concrete type, so case fmt.Stringer catches anything with a String method. Cases are tried from the top, which is why Circle comes first.

Errors: wrap, check, unwrap

Each layer adds what it was doing with %w, so the final message reads like a trail, and the caller can still ask what went wrong underneath with errors.Is and errors.AsType.

package main
 
import (
	"errors"
	"fmt"
	"io/fs"
	"os"
	"strconv"
	"strings"
)
 
type ConfigError struct {
	Line int
	Msg  string
}
 
func (e *ConfigError) Error() string {
	return fmt.Sprintf("line %d: %s", e.Line, e.Msg)
}
 
func parsePort(text string) (int, error) {
	for i, line := range strings.Split(text, "\n") {
		key, value, ok := strings.Cut(line, "=")
		if !ok || strings.TrimSpace(key) != "port" {
			continue
		}
		port, err := strconv.Atoi(strings.TrimSpace(value))
		if err != nil {
			return 0, &ConfigError{Line: i + 1, Msg: "port is not a number"}
		}
		return port, nil
	}
	return 80, nil
}
 
func loadPort(path string) (int, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return 0, fmt.Errorf("load config: %w", err)
	}
	port, err := parsePort(string(data))
	if err != nil {
		return 0, fmt.Errorf("load config %s: %w", path, err)
	}
	return port, nil
}
 
func main() {
	os.WriteFile("bad.conf", []byte("host = localhost\nport = eighty\n"), 0o644)
	defer os.Remove("bad.conf")
 
	for _, path := range []string{"missing.conf", "bad.conf"} {
		_, err := loadPort(path)
		fmt.Println(err)
 
		if errors.Is(err, fs.ErrNotExist) {
			fmt.Println("  -> no file, using the defaults")
		}
		if ce, ok := errors.AsType[*ConfigError](err); ok {
			fmt.Println("  -> fix line", ce.Line)
		}
	}
	// load config: open missing.conf: no such file or directory
	//   -> no file, using the defaults
	// load config bad.conf: line 2: port is not a number
	//   -> fix line 2
}

errors.AsType arrived in Go 1.26. Before it, declare the target and pass a pointer to it: var ce *ConfigError then if errors.As(err, &ce) { ... }.

A worker pool with goroutines, channels and select

Three workers read jobs from one channel and send results on another. A WaitGroup closes the results channel once every worker has finished, and a context with a timeout stops the whole pool if it runs too long.

package main
 
import (
	"context"
	"fmt"
	"slices"
	"sync"
	"time"
)
 
func worker(ctx context.Context, jobs <-chan int, results chan<- string) {
	for n := range jobs {
		select {
		case <-ctx.Done():
			return // timed out: stop taking work
		case <-time.After(10 * time.Millisecond): // pretend to work
		}
		results <- fmt.Sprintf("%d squared is %d", n, n*n)
	}
}
 
func main() {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
 
	jobs := make(chan int)
	results := make(chan string)
 
	var wg sync.WaitGroup
	for range 3 {
		wg.Go(func() { worker(ctx, jobs, results) })
	}
 
	go func() {
		defer close(jobs) // lets each worker's range loop end
		for n := 1; n <= 5; n++ {
			select {
			case jobs <- n:
			case <-ctx.Done():
				return
			}
		}
	}()
 
	go func() {
		wg.Wait()
		close(results) // lets the range below end
	}()
 
	var lines []string
	for line := range results {
		lines = append(lines, line)
	}
	slices.Sort(lines) // workers finish in any order
	for _, line := range lines {
		fmt.Println(line)
	}
	fmt.Println("error:", ctx.Err())
	// 1 squared is 1
	// 2 squared is 4
	// 3 squared is 9
	// 4 squared is 16
	// 5 squared is 25
	// error: <nil>
}

Every channel has exactly one closer: the sender goroutine closes jobs, and the goroutine that waits on the workers closes results. wg.Go arrived in Go 1.25. Before it, write wg.Add(1) and then go func() { defer wg.Done(); worker(ctx, jobs, results) }().

A generic stack

One type that works for any element type, with a generic method that turns a Stack[T] into a slice of something else.

package main
 
import (
	"fmt"
	"strings"
)
 
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
}
 
// Map declares its own type parameter U: a generic method.
func (s *Stack[T]) Map[U any](f func(T) U) []U {
	out := make([]U, 0, len(s.items))
	for _, v := range s.items {
		out = append(out, f(v))
	}
	return out
}
 
func main() {
	var words Stack[string]
	words.Push("go")
	words.Push("gopher")
	fmt.Println(words.Map(strings.ToUpper)) // [GO GOPHER]
 
	lengths := words.Map(func(s string) int { return len(s) })
	fmt.Println(lengths) // [2 6]
 
	top, ok := words.Pop()
	fmt.Println(top, ok) // gopher true
	words.Pop()
	_, ok = words.Pop()
	fmt.Println(ok) // false: empty, and no panic
	// words.Push(42)  // cannot use 42 (untyped int constant) as string value
}

Generic methods arrived in Go 1.27. Before it, Map has to be a plain function that takes the stack: func Map[T, U any](s *Stack[T], f func(T) U) []U.

Gotchas

The mistakes that turn up in almost every Go codebase at some point.

Looks rightWhat actually happensDo this instead
var m map[string]int then m["a"] = 1panic: assignment to entry in nil mapm := make(map[string]int) or map[string]int{}
b := a[:2] then b = append(b, 99)Overwrites a[2]: both share one arrayslices.Clone(a[:2]), or a[:2:2] to cap it
for _, p := range people { p.Age++ }Changes a copy. people is untouchedfor i := range people { people[i].Age++ }
var p *MyErr = nil returned as an errorerr != nil is true: the interface holds a typed nilReturn a plain nil on success
string(65)"A", the character with code 65. go vet flags itstrconv.Itoa(65)
len("héllo")6: it counts bytes, not charactersutf8.RuneCountInString(s)
7 / 23: integer division7.0 / 2, or float64(a) / float64(b)
x, err := f() inside an if blockA new err that hides the outer one, which stays nilx, err = f() with = when err already exists
defer f.Close() inside a loop over filesNothing closes until the function returns, so files pile upMove the loop body into its own function
go work() at the end of mainmain returns and the program ends, most likely before work has runWait for it: sync.WaitGroup or a channel
Two goroutines writing one mapfatal error: concurrent map writesA sync.Mutex, or sync.Map
t.Format("YYYY-MM-DD")YYYY-MM-DD, word for wordt.Format("2006-01-02") or time.DateOnly
http.Get(url) returned no errorThe status can still be 404 or 500Check resp.StatusCode, and close resp.Body
Loop variable captured by a goroutine, on a module with go 1.21 in go.modEvery goroutine sees the value the loop ended onSet go 1.22 or later in go.mod, which gives each iteration its own variable
Iterating a map and expecting insertion orderA different order on every runslices.Sorted(maps.Keys(m))
A struct field name string in JSONLeft out: only exported (capitalised) fields are encodedName string with a json:"name" tag

Common questions

Which version of Go does this cheat sheet cover?

Go 1.27, the current release, checked with Go 1.27.1. Go puts out a release every six months and supports the latest two, but plenty of machines have something older: Ubuntu 24.04 LTS installs Go 1.22 from apt. So Go 1.22 is the baseline here, and anything that needs something newer, such as range over iterators (1.23), b.Loop and strings.Lines (1.24), WaitGroup.Go (1.25), new(42) and errors.AsType (1.26) or generic methods and strings.CutLast (1.27), says so in the notes column.

Is it Go or Golang?

Go. Golang is a nickname that came from the old website address, golang.org, and it is still useful as a search term because go on its own is such a common word. The project, the documentation and the command are all just go, and the website is go.dev.

Does Go have classes and inheritance?

No. Go has structs for data and methods that attach to any named type, which covers most of what a class does. Instead of inheritance it has embedding, where one struct includes another and gains its fields and methods, and interfaces, which any type satisfies just by having the right methods. There are no constructors either: a function called NewThing that returns a *Thing is the convention.

Does Go have exceptions?

Not for ordinary failures. A function that can fail returns an error value as its last result, and the caller checks it with if err != nil. Errors are wrapped with context using fmt.Errorf and %w, and inspected with errors.Is and errors.As. Go does have panic and recover, but panic is for bugs and truly unrecoverable states, such as an index out of range, not for a missing file or bad user input.

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

An array has a fixed length that is part of its type, so [3]int and [4]int are different types, and assigning an array copies every element. A slice is a small header that points into an array and records a length and a capacity. It can grow with append, and copying a slice copies only the header, so both copies share the same elements. Almost all Go code uses slices; arrays mostly appear as the storage underneath them.

Should a method have a value receiver or a pointer receiver?

Use a pointer receiver when the method needs to change the struct, when the struct is large enough that copying it on every call matters, or when it contains something that must not be copied, such as a sync.Mutex. Otherwise a value receiver is fine. Keep all of a type's methods the same kind, because the choice decides whether the value, the pointer or both satisfy an interface.

What is the difference between a goroutine and a thread?

A goroutine is 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, and the runtime schedules them across a small pool of OS threads, one per CPU core by default. Starting one is just the go keyword in front of a function call.

Does Go have generics?

Yes, since Go 1.18. Functions and types can take type parameters, such as func Map[T, U any](s []T, f func(T) U) []U, with constraints like any, comparable and cmp.Ordered limiting which types are allowed. The standard library's slices and maps packages are built on them. Go 1.27 added generic methods, so a method can now declare type parameters of its own as well as using its type's.

See all cheat sheets

Want this explained by a cat?

The videos cover the same ground in sixty seconds. If there is a tool you want a cheat sheet for next, ask.