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.
Searches the task, the command and the third column. Press / from anywhere on the page.
304 commands
The go command
| Task | Command | Notes |
|---|---|---|
| Check which version you have | go version | |
| Start a module | go mod init example.com/hello | Writes go.mod. The path is what other code imports it by |
| Run the program in this folder | go run . | Compiles to a temporary folder, runs it, and cleans up |
| Pass arguments to your program | go run . -port 8080 | Everything after the package goes to os.Args |
| Build a binary | go build go build -o bin/app . | Without -o the binary is named after the module or folder |
| Build for another OS | GOOS=linux GOARCH=arm64 go build -o app-linux . | No extra toolchain needed. go tool dist list shows every pair |
| Build with no C dependencies | CGO_ENABLED=0 go build . | A fully static binary that runs in a scratch Docker image |
| Smaller binary | go build -ldflags="-s -w" . | Drops the symbol table and debug info |
| Set a variable at build time | go build -ldflags="-X main.version=1.2.3" . | version must be a package-level string var |
| Add or upgrade a dependency | go get github.com/google/go-cmp@latest | Updates go.mod and go.sum. @v0.7.0 pins a version |
| Upgrade every dependency | go get -u ./... | -u=patch for bug-fix releases only |
| Add missing and drop unused | go mod tidy | Run it before every commit that touches imports |
| Why is this dependency here | go mod why -m golang.org/x/text | |
| List dependencies, and upgrades | go list -m all go list -m -u all | |
| Use a local copy of a dependency | go mod edit -replace example.com/lib=../lib | Adds a replace line to go.mod. -dropreplace undoes it |
| Work on several modules together | go work init ./api ./shared | Writes go.work, which wins over the replace lines |
| Install a command-line program | go install golang.org/x/tools/cmd/stringer@latest | Lands in $(go env GOPATH)/bin, usually ~/go/bin |
| Track a tool in go.mod | go get -tool golang.org/x/tools/cmd/stringer | Go 1.24. Then go tool stringer runs that exact version |
| Format the code | go fmt ./... gofmt -l . | gofmt -l lists files that are not formatted. There is no style debate |
| Catch likely bugs | go vet ./... | Printf mistakes, unkeyed struct fields, copied locks and more |
| Update code to newer idioms | go fix -diff ./... | Go 1.26. Drop -diff to apply them, for example min and max over if statements |
| Run the tests | go test ./... | ./... means this folder and every one below it |
| Run some of the tests | go test -run 'TestParse/empty' ./parser | A regex, with / to pick a subtest |
| Verbose, and skip the cache | go test -v -count=1 ./... | Passing results are cached until the code changes |
| Find data races | go test -race ./... go run -race . | Slower, so it is for tests and debugging |
| Test coverage | go test -coverprofile=cover.out ./... go tool cover -html=cover.out | -cover alone prints a percentage |
| Benchmarks | go test -bench=. -benchmem | The tests run too, unless you add -run='^$' |
| Read the docs for something | go doc strings.Cut go doc -all net/http | Offline. pkg.go.dev has the same docs on the web |
| See the build settings | go env GOPATH GOOS GOARCH | go env -w GOPRIVATE=example.com/* saves one |
| Run go:generate lines | go generate ./... | Not part of go build. You run it and commit the output |
| Clear the caches | go 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.
| Task | Code | Notes |
|---|---|---|
| Make a program | package main func main() { fmt.Println("hi") } | package main plus func main is a program. Any other name is a library |
| Import one package | import "fmt" | |
| Import several | import ("fmt"; "os") | gofmt puts one per line. Unused imports do not compile |
| Rename an import | import str "strings" | Or to avoid a clash between two packages called rand |
| Import only for its side effects | import _ "image/png" | Runs its init functions. This one registers the PNG decoder |
| Import a package from your module | import "example.com/shop/store" | The module path plus the folder |
| Exported or not | func Parse() {} func parse() {} | Capitalised means visible to other packages. There is no public keyword |
| Run setup when the package loads | func init() { } | Before main, once per package. Keep it small |
| What go.mod holds | module example.com/shop go 1.22 require github.com/google/go-cmp v0.7.0 | The go line is the oldest Go the module needs, and sets the language version |
| Code only your module can import | example.com/shop/internal/store | Anything under internal/ is importable only from inside its parent folder |
| Bundle files into the binary | //go:embed static var static embed.FS | Also into a string or []byte for one file. Needs import embed |
| Build a file only on one OS | //go:build linux | First line of the file. Or name the file store_linux.go |
| Tests live beside the code | store.go store_test.go | Files 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.
| Task | Code | Notes |
|---|---|---|
| Declare and infer the type | name := "Ada" | Inside functions only |
| Declare with a type | var count int | Starts at the zero value: 0, "", false or nil. Nothing is ever uninitialised |
| Several at once | a, b := 1, "two" var x, y float64 | := needs at least one new name on the left |
| Package-level variable | var debug = false | := does not work outside a function |
| Constant | const MaxUsers = 100 | Numbers, strings, bools and runes. Untyped until used |
| Numbered constants | const ( Small = iota; Medium; Large ) | iota is 0, 1, 2 within one const block |
| An enum with its own type | type Size int const ( Small Size = iota; Medium; Large ) | Go has no enum keyword. This is the idiom |
| Integers | int int64 int32 uint8 uint | int is 64 bits on 64-bit platforms. byte is uint8, rune is int32 |
| Floating point | f := 3.14 | float64 unless you say float32 |
| Boolean and string | ok := true s := "text" | |
| Number literals | 1_000_000 0xFF 0o755 0b1010 | The _ is a digit separator |
| Convert between number types | float64(n) int(f) int64(n) | Always explicit. int(f) truncates, so 3.9 becomes 3 |
| Number to string | strconv.Itoa(42) | Not string(42), which gives "*", the character with code 42 |
| String to number | n, err := strconv.Atoi("42") | Returns an error for bad input instead of panicking |
| Other parses | strconv.ParseFloat("2.5", 64) strconv.ParseBool("true") | |
| Largest and smallest values | math.MaxInt math.MaxInt64 math.MinInt32 | Integers wrap silently on overflow |
| Pointer to a variable | p := &x *p = 10 | No pointer arithmetic. p.Field works without -> |
| Pointer to a new zero value | p := new(int) | Same as var v int; p := &v |
| Pointer to a value in one step | p := new(42) | Go 1.26. Handy for optional fields like *int in structs |
| A new type from an old one | type Celsius float64 | A distinct type: you cannot add a Celsius to a float64 without converting |
| Another name for a type | type Text = string | An alias. Text and string are interchangeable |
| Generic alias | type Set[T comparable] = map[T]struct{} | Go 1.24 |
| Any type at all | var v any = 42 | any is short for interface{} |
| Smallest and largest of some values | min(a, b, c) max(x, y) | Built in. Work on numbers and strings |
| Print a value's type | fmt.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.
| Task | Code | Notes |
|---|---|---|
| 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 strings | s := first + " " + last | |
| Length in bytes, and in characters | len("héllo") utf8.RuneCountInString("héllo") | 6 and 5. unicode/utf8 |
| One byte | s[0] | A byte, not a character. s[0] = 'x' does not compile |
| Loop over the characters | for i, r := range s { } | r is a rune and i is its byte offset |
| Part of a string | s[1:4] s[:3] s[2:] | Byte positions. Start included, end excluded |
| Contains, prefix, suffix | strings.Contains(s, "cat") strings.HasPrefix(s, "/") strings.HasSuffix(s, ".go") | |
| Find text | strings.Index(s, "cat") | -1 if it is not there |
| Split and join | strings.Split("a,b,c", ",") strings.Join(parts, ", ") | |
| Split on any whitespace | strings.Fields(s) | Drops empty pieces, unlike Split |
| Split around the first match | key, value, ok := strings.Cut("port=8080", "=") | |
| Split around the last match | dir, file, ok := strings.CutLast("a/b/c.txt", "/") | Go 1.27. dir is a/b, file is c.txt |
| Replace | strings.ReplaceAll(s, "cat", "dog") strings.Replace(s, "cat", "dog", 1) | The 1 is how many to replace |
| Trim | strings.TrimSpace(s) strings.TrimPrefix(s, "v") strings.TrimSuffix(s, ".go") | |
| Change case | strings.ToUpper(s) strings.ToLower(s) | |
| Compare ignoring case | strings.EqualFold(a, b) | |
| Compare | a == b a < b | == compares the text. < is byte order |
| Repeat | strings.Repeat("-", 20) | |
| Build one up in a loop | var sb strings.Builder sb.WriteString(word) s := sb.String() | += in a loop copies the whole string every time |
| Loop over the lines | for line := range strings.Lines(text) { } | Go 1.24. Each line keeps its trailing newline |
| Loop over the pieces without a slice | for 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 character | unicode.IsDigit(r) unicode.IsLetter(r) unicode.IsSpace(r) | |
| Print with spaces and a newline | fmt.Println("total:", total) | |
| Print with a format | fmt.Printf("%s is %d\n", name, age) | Printf adds no newline |
| Format into a string | s := fmt.Sprintf("%.2f", price) | |
| The formatting verbs | %v %+v %#v %T %d %s %q %x %t %p | Any 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 error | fmt.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.
| Task | Code | Notes |
|---|---|---|
| Array | var a [3]int b := [...]int{1, 2, 3} | [3]int and [4]int are different types. Arrays copy on assignment |
| Slice with values | nums := []int{3, 1, 2} | |
| Empty slice with room to grow | s := make([]int, 0, 100) | Length 0, capacity 100. Saves re-allocating in a loop |
| Slice of zeros | s := make([]int, 5) | Length 5. append adds after the zeros, not over them |
| Add to the end | nums = append(nums, 4, 5) | Always assign the result: append may return a new array |
| Add another slice | a = append(a, b...) | |
| Length and capacity | len(nums) cap(nums) | |
| Part of a slice | nums[1:3] nums[:2] nums[2:] | Shares the array with nums, so writes show through |
| Part, with no room to grow | nums[1:3:3] | The third number caps capacity, so a later append copies instead of overwriting nums |
| Loop | for i, v := range nums { } | v is a copy. Write through nums[i] to change the slice |
| Copy | c := slices.Clone(nums) | Or copy(dst, src), which copies min(len(dst), len(src)) items |
| Combine several | all := slices.Concat(a, b, c) | |
| Contains and find | slices.Contains(nums, 3) slices.Index(nums, 3) | Index gives -1 if it is not there |
| Find with a condition | i := slices.IndexFunc(people, func(p Person) bool { return p.Age > 30 }) | |
| Sort | slices.Sort(nums) | In place. Numbers, strings, anything ordered |
| Sort by a field | slices.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 fields | cmp.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 slice | i, found := slices.BinarySearch(sorted, 7) | |
| Smallest, largest, reverse | slices.Min(nums) slices.Max(nums) slices.Reverse(nums) | Min and Max panic on an empty slice |
| Equal | slices.Equal(a, b) | == only compares a slice with nil |
| Insert and delete | nums = slices.Insert(nums, 1, 99) nums = slices.Delete(nums, 1, 2) | Delete removes items 1 up to, not including, 2 |
| Remove every match | nums = slices.DeleteFunc(nums, func(n int) bool { return n < 0 }) | |
| Drop repeats in a sorted slice | nums = slices.Compact(nums) | |
| Batches of three | for batch := range slices.Chunk(nums, 3) { } | Go 1.23 |
| Collect an iterator into a slice | keys := slices.Collect(maps.Keys(m)) | Go 1.23 |
| Grid | grid := 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 zero | clear(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.
| Task | Code | Notes |
|---|---|---|
| Map with values | ages := map[string]int{"Ada": 36, "Linus": 29} | |
| Empty map | m := make(map[string]int) | Not var m map[string]int, which is nil and panics on write |
| Add or replace | ages["Bob"] = 40 | |
| Look up | ages["Zoe"] | 0 for a missing key, with no error |
| Look up and check it was there | age, ok := ages["Zoe"] | The comma ok idiom. ok is false for a missing key |
| Only if it is there | if age, ok := ages["Ada"]; ok { } | |
| Delete | delete(ages, "Ada") | Does nothing if the key is missing |
| How many | len(ages) | |
| Loop | for name, age := range ages { } | The order is random on purpose, and changes from run to run |
| Loop in key order | for _, name := range slices.Sorted(maps.Keys(ages)) { } | Go 1.23 |
| Count occurrences | counts[word]++ | A missing key starts at 0 |
| Group into lists | byCity[p.City] = append(byCity[p.City], p) | A missing key gives a nil slice, which append accepts |
| A set | seen := map[string]bool{} seen[x] = true if seen[x] { } | Or map[string]struct{} to store nothing per key |
| Struct as the key | type Point struct{ X, Y int } grid := map[Point]string{} | Every field must be comparable |
| Copy | c := maps.Clone(ages) maps.Copy(dst, src) | |
| Equal | maps.Equal(a, b) | |
| Delete every match | maps.DeleteFunc(ages, func(k string, v int) bool { return v < 18 }) | |
| Remove everything | clear(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.
| Task | Code | Notes |
|---|---|---|
| if, else if, else | if n > 0 { } else if n < 0 { } else { } | The condition must be a bool. if n { } does not compile |
| if with a setup statement | if err := save(); err != nil { } | err exists only inside the if and its else |
| Pick one of two values | label := "odd" if n%2 == 0 { label = "even" } | Go has no ?: operator |
| Counting loop | for i := 0; i < 10; i++ { } | |
| Count to n | for i := range 10 { } | 0 to 9 |
| Loop over a slice, map, string or channel | for i, v := range items { } | for _, v to skip the index, for i := range for just the index |
| while loop | for n > 1 { } | |
| Loop forever | for { } | Leave with break or return |
| Skip to the next round, or stop | continue break | |
| Leave nested loops | outer: for _, row := range grid { for _, v := range row { if v < 0 { break outer } } } | A labelled break. continue outer works too |
| switch | switch day { case "sat", "sun": rest() default: work() } | No fall-through and no break needed. A case can list several values |
| switch with conditions | switch { 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 case | fallthrough | The last statement of a case. Rarely needed |
| Run something when the function returns | defer f.Close() | Runs however the function ends, even on a panic. Several run last in, first out |
| Loop over an iterator | for 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.
| Task | Code | Notes |
|---|---|---|
| Define | func add(a, b int) int { return a + b } | Parameters of the same type can share it |
| Return two values | func divmod(a, b int) (int, int) { return a / b, a % b } | |
| Take both | q, r := divmod(7, 2) | 3 and 1 |
| Ignore one | q, _ := divmod(7, 2) | _ throws a value away. An unused variable does not compile |
| Return a value or an error | func load(path string) ([]byte, error) | The error goes last |
| Named results | func 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 arguments | func sum(nums ...int) int | nums is a []int inside |
| Pass a slice to it | sum(1, 2, 3) sum(nums...) | |
| Change the caller's variable | func reset(n *int) { *n = 0 } reset(&count) | |
| Function in a variable | square := func(x int) int { return x * x } | |
| Function as a parameter | func apply(f func(int) int, x int) int { return f(x) } | |
| Closure that keeps state | func counter() func() int { n := 0; return func() int { n++; return n } } | Each call to counter gets its own n |
| Call a function literal at once | func() { fmt.Println("now") }() | Common with defer and go |
| Generic function | func Map[T, U any](s []T, f func(T) U) []U | Type arguments are usually inferred: Map(nums, strconv.Itoa) |
| Only ordered types | func Largest[T cmp.Ordered](s []T) T | Numbers and strings: anything < works on |
| Your own constraint | type Number interface { ~int | ~int64 | ~float64 } | ~int also accepts types defined on int, like type Celsius int |
| Write your own iterator | func 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.
| Task | Code | Notes |
|---|---|---|
| Define a struct | type Point struct { X, Y int } | |
| Create one | p := Point{X: 1, Y: 2} | Fields you leave out get their zero value |
| Create one by position | Point{1, 2} | Breaks when a field is added. go vet flags it for other packages' types |
| Create a pointer to one | p := &Point{X: 1} | p.X works on a pointer too |
| Read and change a field | p.X p.X = 10 | |
| One-off struct | cfg := struct { Host string; Port int }{"localhost", 8080} | An anonymous struct. Handy in tests and for JSON |
| Compare | a == b | Works when every field is comparable |
| Method | func (p Point) Dist() float64 { return math.Hypot(float64(p.X), float64(p.Y)) } | p is the receiver, and a copy |
| Method that changes the struct | func (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) |
| Constructor | func NewServer(addr string) *Server { return &Server{addr: addr, timeout: 30 * time.Second} } | Just a function. NewX is the convention |
| Embed a struct | type Admin struct { User; Level int } | Admin gets User's fields and methods: a.Name, a.Greet() |
| Reach the embedded one | a.User.Name | Always works, even when the short form is ambiguous |
| Set a promoted field in a literal | Admin{Name: "Ada", Level: 2} | Go 1.27. Before it, Admin{User: User{Name: "Ada"}, Level: 2} |
| Methods on any named type | type Celsius float64 func (c Celsius) F() float64 { return float64(c)*9/5 + 32 } | |
| Custom text for printing | func (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 names | fmt.Printf("%+v\n", p) | {X:1 Y:2} |
| Struct tags | Name string `json:"name,omitempty"` | Read by packages such as encoding/json. omitempty skips 0, "", nil and empty slices |
| Leave out zero structs in JSON | Created 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.
| Task | Code | Notes |
|---|---|---|
| Define | type Shape interface { Area() float64 } | |
| Satisfy it | func (c Circle) Area() float64 { return math.Pi * c.R * c.R } | That is all. Circle is now a Shape |
| Accept any Shape | func Total(shapes ...Shape) float64 | |
| Check at compile time | var _ Shape = (*Square)(nil) | Fails to build if *Square stops satisfying Shape |
| Get the concrete type back | c, ok := s.(Circle) | A type assertion. Without ok it panics on the wrong type |
| Branch on the type | switch 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 method | if s, ok := v.(fmt.Stringer); ok { fmt.Println(s.String()) } | |
| Combine interfaces | type ReadWriter interface { io.Reader; io.Writer } | |
| Accept anything | func Log(v any) | Then a type switch to do something with it |
| The ones you meet everywhere | error fmt.Stringer io.Reader io.Writer http.Handler | |
| Methods with pointer receivers | var 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
| Task | Code | Notes |
|---|---|---|
| Generic type | type Stack[T any] struct { items []T } | |
| Method on it | func (s *Stack[T]) Push(v T) { s.items = append(s.items, v) } | |
| Use it | var s Stack[string] s.Push("a") | |
| The zero value of T | var zero T | For returning nothing from a generic function |
| Keys you can compare with == | func Unique[T comparable](s []T) []T | comparable is what map keys need |
| Several type parameters | type Pair[K comparable, V any] struct { Key K; Value V } | |
| Method with its own type parameter | func (s *Stack[T]) Map[U any](f func(T) U) []U | Go 1.27. Called as s.Map(strings.ToUpper), with U inferred |
| Say the types yourself | Map[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.
| Task | Code | Notes |
|---|---|---|
| Check an error | data, err := os.ReadFile(path) if err != nil { return err } | |
| Make one | errors.New("name is empty") | |
| Make one with values | fmt.Errorf("port %d is out of range", port) | |
| Add context and keep the cause | return 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 for | var ErrNotFound = errors.New("not found") | A sentinel error. Name it ErrSomething |
| Is it that error, at any depth | if errors.Is(err, fs.ErrNotExist) { } | Looks through every wrapped layer. == only checks the outer one |
| Your own error type | type 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 it | var ve *ValidationError if errors.As(err, &ve) { fmt.Println(ve.Field) } | |
| The same, in one line | if ve, ok := errors.AsType[*ValidationError](err); ok { } | Go 1.26 |
| Several errors as one | err := errors.Join(err1, err2) | nil if all of them are nil. Is and As check each one |
| Stop the program: a bug | panic("unreachable") | For things that should never happen, not for bad input |
| Recover from a panic | defer func() { if r := recover(); r != nil { log.Println("recovered:", r) } }() | Only works inside a deferred function |
| Print and exit | log.Fatal(err) | Exits with status 1, and deferred calls do not run |
| Exit with a status | os.Exit(2) | Also skips deferred calls |
| Handle a Close error on a file you wrote | defer 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.
| Task | Code | Notes |
|---|---|---|
| Start a goroutine | go fetch(url) | Returns at once. Any return value is thrown away |
| Start several, wait for all | var 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.25 | wg.Add(1) go func() { defer wg.Done(); fetch(u) }() | Call Add before go, not inside the goroutine |
| Make a channel | ch := make(chan int) | Unbuffered: a send waits for a receiver |
| Channel with a buffer | ch := make(chan int, 10) | Sends only wait when 10 are waiting to be read |
| Send and receive | ch <- 42 v := <-ch | |
| Close it | close(ch) | The sender closes, never the receiver. Sending on a closed channel panics |
| Receive until it is closed | for v := range ch { } | Ends when ch is closed and empty |
| Was it closed | v, ok := <-ch | ok is false once ch is closed and empty |
| Send-only and receive-only | func produce(out chan<- int) func consume(in <-chan int) | The compiler stops the wrong direction |
| Protect shared data | var mu sync.Mutex mu.Lock() defer mu.Unlock() | Put the mutex in the struct beside what it guards |
| Many readers, one writer | var mu sync.RWMutex mu.RLock() defer mu.RUnlock() | |
| A counter without a lock | var hits atomic.Int64 hits.Add(1) hits.Load() | sync/atomic |
| Run something once, lazily | var config = sync.OnceValue(loadConfig) cfg := config() | loadConfig runs on the first call only, even from many goroutines |
| At most four at a time | sem := make(chan struct{}, 4) sem <- struct{}{} defer func() { <-sem }() | A buffered channel as a semaphore |
| Run several and stop at the first error | g, 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 parallel | runtime.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.
| Task | Code | Notes |
|---|---|---|
| Wait on several channels | select { case msg := <-inbox: handle(msg); case err := <-errs: return err } | If several are ready, it picks one at random |
| Give up after a time | select { case res := <-results: use(res); case <-time.After(2 * time.Second): return errors.New("timed out") } | |
| Do not wait at all | select { case ch <- v: default: dropped++ } | default runs when nothing else is ready |
| Stop when told to | case <-done: return | Close done to tell every goroutine watching it at once |
| Do something every second | t := time.NewTicker(time.Second) defer t.Stop() for range t.C { } | |
| Switch a case off | inbox = nil | A nil channel is never ready, so select skips that case |
| Cancel after a time | ctx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() | Always call cancel, or the timer leaks until it fires |
| Cancel by hand | ctx, cancel := context.WithCancel(context.Background()) | |
| Stop when the context ends | case <-ctx.Done(): return ctx.Err() | context.Canceled or context.DeadlineExceeded |
| Take a context | func Fetch(ctx context.Context, url string) error | The first parameter, named ctx, by convention |
| HTTP request that can be cancelled | req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) | |
| Cancel on Ctrl-C | ctx, 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.
| Task | Code | Notes |
|---|---|---|
| A test | func 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 test | t.Fatalf("open: %v", err) | |
| Table-driven tests | for _, 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 parallel | t.Parallel() | |
| Report failures at the caller's line | t.Helper() | First line of a helper function |
| Temporary folder | dir := t.TempDir() | Deleted when the test ends |
| Clean up afterwards | t.Cleanup(func() { db.Close() }) | |
| A context for the test | ctx := t.Context() | Go 1.24. Cancelled just before Cleanup functions run |
| Skip | t.Skip("needs a database") | Or if testing.Short() { t.Skip() } with go test -short |
| Benchmark | func BenchmarkAdd(b *testing.B) { for b.Loop() { Add(2, 3) } } | Go 1.24. Before it, for range b.N |
| Example that doubles as a test | func 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 test | func 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 handler | rec := httptest.NewRecorder() handler(rec, httptest.NewRequest("GET", "/users/1", nil)) | rec.Code and rec.Body hold the response |
| A real server for a test | srv := httptest.NewTestServer(t, handler) | Go 1.27. In memory, and closed when the test ends. httptest.NewServer before that |
Files, JSON, time and HTTP
| Task | Code | Notes |
|---|---|---|
| Read a whole file | data, err := os.ReadFile("notes.txt") | A []byte. string(data) for text |
| Write a file | err := os.WriteFile("out.txt", data, 0o644) | Replaces it. 0o644 is the permission for a new file |
| Read line by line | sc := 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 close | f, 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 file | f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) | |
| Does it exist | if _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) { } | |
| Make folders | os.MkdirAll("out/logs", 0o755) | Fine if they already exist |
| List a folder | entries, err := os.ReadDir(".") | e.Name() and e.IsDir() on each entry |
| Build a path | filepath.Join("data", "users.json") | The right separator for the OS |
| Command-line arguments | os.Args[1:] | os.Args[0] is the program |
| Command-line flags | port := flag.Int("port", 8080, "port to listen on") flag.Parse() | *port for the value. -h prints the help for you |
| Environment variable | os.Getenv("HOME") v, ok := os.LookupEnv("TOKEN") | Getenv gives "" when it is not set |
| Struct to JSON | data, err := json.Marshal(user) | Only exported fields. MarshalIndent for pretty output |
| JSON to a struct | var u User err := json.Unmarshal(data, &u) | Pass a pointer. Unknown fields are ignored |
| JSON straight from a stream | err := json.NewDecoder(resp.Body).Decode(&u) | |
| The newer JSON package | import "encoding/json/v2" | Go 1.27. Stricter and faster, with the same Marshal and Unmarshal. encoding/json stays |
| Now, and time since | start := time.Now() elapsed := time.Since(start) | |
| Pause | time.Sleep(500 * time.Millisecond) | |
| A length of time | d := 90 * time.Minute d.Hours() | 1.5. Durations print as 1h30m0s |
| Date arithmetic | t.Add(24 * time.Hour) t.AddDate(0, 1, 0) t2.Sub(t1) | |
| Format a time | t.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 time | t, err := time.Parse(time.DateOnly, "2026-09-27") | time.DateOnly is "2006-01-02" |
| HTTP GET | resp, 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 timeout | client := &http.Client{Timeout: 10 * time.Second} | http.Get and the default client never time out |
| Web server with routes | http.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 it | log.Fatal(http.ListenAndServe(":8080", nil)) | |
| Send JSON back | w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(v) | |
| Structured logging | slog.Info("started", "port", 8080) | log/slog. slog.New(slog.NewJSONHandler(os.Stdout, nil)) for JSON lines |
| Random number | rand.IntN(6) + 1 | math/rand/v2. 1 to 6. crypto/rand for anything secret |
| Unique ID | id := uuid.New() uuid.NewV7() | Go 1.27. Package uuid. Version 7 sorts by creation time |
| Match a regex | re := 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 regex | re.ReplaceAllString(s, " ") | |
| Run another program | out, 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 right | What actually happens | Do this instead |
|---|---|---|
var m map[string]int then m["a"] = 1 | panic: assignment to entry in nil map | m := make(map[string]int) or map[string]int{} |
b := a[:2] then b = append(b, 99) | Overwrites a[2]: both share one array | slices.Clone(a[:2]), or a[:2:2] to cap it |
for _, p := range people { p.Age++ } | Changes a copy. people is untouched | for i := range people { people[i].Age++ } |
var p *MyErr = nil returned as an error | err != nil is true: the interface holds a typed nil | Return a plain nil on success |
string(65) | "A", the character with code 65. go vet flags it | strconv.Itoa(65) |
len("héllo") | 6: it counts bytes, not characters | utf8.RuneCountInString(s) |
7 / 2 | 3: integer division | 7.0 / 2, or float64(a) / float64(b) |
x, err := f() inside an if block | A new err that hides the outer one, which stays nil | x, err = f() with = when err already exists |
defer f.Close() inside a loop over files | Nothing closes until the function returns, so files pile up | Move the loop body into its own function |
go work() at the end of main | main returns and the program ends, most likely before work has run | Wait for it: sync.WaitGroup or a channel |
| Two goroutines writing one map | fatal error: concurrent map writes | A sync.Mutex, or sync.Map |
t.Format("YYYY-MM-DD") | YYYY-MM-DD, word for word | t.Format("2006-01-02") or time.DateOnly |
http.Get(url) returned no error | The status can still be 404 or 500 | Check resp.StatusCode, and close resp.Body |
Loop variable captured by a goroutine, on a module with go 1.21 in go.mod | Every goroutine sees the value the loop ended on | Set go 1.22 or later in go.mod, which gives each iteration its own variable |
| Iterating a map and expecting insertion order | A different order on every run | slices.Sorted(maps.Keys(m)) |
A struct field name string in JSON | Left out: only exported (capitalised) fields are encoded | Name 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.
