GoCheatsheet

Go Syntax Cheatsheet📋

The Go syntax and patterns you actually use daily, organized so you stop context-switching to pkg.go.dev. Covers core constructs through concurrency primitives with idiomatic Go 1.22+ examples and tips that save you a debugging session.

Quick Reference

NameSyntaxDescription
Short Variable Declarationx := 42Declares and initializes inside a function. The compiler infers the type from the right-hand side.
Multiple Return Valuesfunc f() (int, error) { return 0, nil }Functions can return multiple values. Used everywhere for result + error pairs.
Struct Literalp := Point{X: 1, Y: 2}Initialize structs with named fields. Unset fields get their zero value.
Slice Creations := make([]int, 0, 10)Allocate a slice with length 0 and capacity 10. Avoids repeated allocations during append.
Map Initializationm := map[string]int{"a": 1}Create and populate a map in one expression. Keys must be comparable types.
Goroutinego func() { /* ... */ }()Launch a function on a new goroutine. Lightweight — thousands are normal.
Channel Send/Receivech <- v / v := <-chSend a value into a channel or receive from it. Unbuffered channels block until both sides are ready.
Deferdefer f.Close()Schedule a call to run when the enclosing function returns. Executes in LIFO order.
Error Checkif err != nil { return err }The standard Go error-handling pattern. Check immediately after every fallible call.
Type Assertionv, ok := i.(string)Extract the concrete type from an interface. The comma-ok form avoids panics.
Range Loopfor i, v := range slice { }Iterate over slices, maps, strings, or channels. Use _ to discard index or value.
Interface Definitiontype Reader interface { Read([]byte) (int, error) }Interfaces are satisfied implicitly — no implements keyword. Keep them small.
Pointer Receiverfunc (s *Server) Start() error { }Method that can mutate the receiver. Use pointer receivers for large structs or mutation.
Select Statementselect { case v := <-ch: ... default: ... }Multiplex across channel operations. default makes it non-blocking.
Blank Identifier_, err := io.Copy(dst, src)Discard a value you don't need. Common when only the error matters.

Variables & Types

Variable Declarations

var x int / x := 42 / const Pi = 3.14

Go has two declaration styles: var for package-level or when the type differs from the initializer, and := for concise local declarations. Constants are computed at compile time and can use iota for sequential values.

go

Tips

  • Use := inside functions for brevity. Reserve var for package-level variables or when you need an explicit type that differs from the initializer.
  • Uninitialized variables get their zero value: 0 for numbers, "" for strings, nil for pointers/slices/maps, false for bools.
  • iota resets to 0 at each const block. Use _ = iota to skip the zero value when it should be invalid.

Basic Types & Zero Values

int, float64, string, bool, byte, rune

Go's type system is explicit with no implicit conversions between numeric types. Every type has a well-defined zero value, which makes uninitialized variables safe to use in most contexts.

go

Tips

  • Use int for general integers. Only use sized types (int32, int64) when the protocol or API requires a specific width.
  • string values are immutable. Repeated concatenation in a loop creates garbage — use strings.Builder instead.
  • byte is uint8, rune is int32. Use rune when working with Unicode characters, byte for raw data.

Type Conversions

T(value)

Go requires explicit type conversions for all numeric types. String conversions go through the strconv package for numbers, or direct conversion for byte/rune slices. Named types based on the same underlying type also require explicit conversion.

go

Tips

  • Converting between int and float64 can lose precision for large values. int64 values above 2^53 will round when converted to float64.
  • []byte(s) and string(b) both allocate a copy. In hot paths, consider unsafe.String or unsafe.Slice if you can guarantee the data won't be mutated.
  • Use fmt.Sprintf for complex formatting. Use strconv for simple int/float conversions — it is faster than fmt.

Functions & Control Flow

Functions & Multiple Returns

func name(params) (returns) { }

Go functions support multiple return values, named returns, variadic parameters, and closures. Functions are first-class values that can be passed as arguments, returned, and stored in variables.

go

Tips

  • Avoid naked returns in functions longer than a few lines — they hurt readability when the reader cannot see the return values at a glance.
  • Variadic parameters receive a slice. Spread an existing slice with the ... suffix: sum(nums...).
  • Closures capture variables by reference. Be cautious in loops — the captured variable may change before the closure executes.

If, Switch & For

if / switch / for

Go has only three control flow statements: if, switch, and for. The for keyword covers traditional loops, while loops, infinite loops, and range iteration. Switch cases don't fall through by default.

go

Tips

  • Use if-init statements to scope variables to the if/else block: if err := doSomething(); err != nil { }.
  • Go 1.22 changed range loop variables to be per-iteration, fixing the classic closure capture bug. Use 'for i := range 5' to iterate over integers.
  • Use fallthrough in switch only when you explicitly need C-style fall-through behavior — it is rare in idiomatic Go.

Defer, Panic & Recover

defer fn() / panic(v) / recover()

defer schedules a function call to run when the surrounding function returns — essential for cleanup. panic halts normal execution and unwinds the stack. recover, called inside a deferred function, regains control after a panic.

go

Tips

  • defer arguments are evaluated immediately, not when the deferred function executes. Use a closure if you need to capture the final value.
  • Use defer with named return values to modify the error before returning — a clean pattern for wrapping errors with context.
  • Reserve panic for truly unrecoverable situations (programmer errors, impossible states). Use error returns for expected failures.

Structs & Interfaces

Struct Declaration & Methods

type Name struct { } / func (s *Name) Method() { }

Structs are Go's primary data-structuring tool. Methods are defined on types with receiver arguments. Embedding provides composition — promoted fields and methods behave as if they belong to the outer struct.

go

Tips

  • Use pointer receivers when the method mutates state, the struct is large, or you need consistency (if one method uses a pointer receiver, all should).
  • Go has no constructors. Use NewX factory functions that return *X — this is a universal convention.
  • Embedded struct fields can be accessed directly (admin.Name) or explicitly (admin.User.Name). The explicit form resolves ambiguity when multiple embeddings expose the same field name.

Interfaces & Implicit Satisfaction

type Name interface { Method() }

Go interfaces are satisfied implicitly — any type that implements the required methods satisfies the interface. This enables polymorphism without coupling. The empty interface (any) accepts all types.

go

Tips

  • Define interfaces where they are consumed, not where they are implemented. This keeps packages decoupled.
  • Keep interfaces small — one or two methods. The io.Reader and io.Writer interfaces are the gold standard.
  • Use var _ Interface = (*Type)(nil) to verify interface satisfaction at compile time without allocating.

Type Assertions & Type Switches

v, ok := i.(Type) / switch v := i.(type) { }

Type assertions extract the concrete value from an interface. The comma-ok form prevents panics on failed assertions. Type switches cleanly handle multiple possible concrete types.

go

Tips

  • Always use the comma-ok form (v, ok := i.(T)) unless you are absolutely certain of the type. A failed assertion without ok panics at runtime.
  • Type switches are the idiomatic way to handle interface values with multiple possible types — cleaner than chained if-else type assertions.
  • If you find yourself writing many type switches on the same interface, consider adding a method to the interface instead.

Generics

func Name[T constraint](params) { }

Generics (Go 1.18+) enable type-parameterized functions and types. Use constraints from the cmp package for ordered types, any for unconstrained types, or define custom constraint interfaces.

go

Tips

  • Prefer the standard library packages slices, maps, and cmp over writing your own generic helpers for common operations.
  • Use generics when you find yourself writing the same function for multiple types. Do not use them when an interface would be clearer.
  • Return the zero value of a generic type with var zero T — there is no syntax like default(T).

Slices, Maps & Strings

Slices

make([]T, len, cap) / append(s, v...) / s[low:high]

Slices are Go's primary sequence type — a dynamic view over an underlying array. The slices package (Go 1.21+) provides generic helpers for sorting, searching, cloning, and manipulating slices without writing boilerplate.

go

Tips

  • Pre-allocate with make([]T, 0, expectedSize) when you know the approximate size. This avoids repeated allocations during append.
  • Slicing does not copy data. Modifications to a sub-slice affect the original. Use the three-index slice s[a:b:b] to detach capacity.
  • A nil slice and an empty slice ([]int{}) behave identically with append, len, and range. But they differ in JSON marshaling: nil becomes null, empty becomes [].

Maps

make(map[K]V) / m[key] = val / delete(m, key)

Maps are built-in hash tables with O(1) average access. Keys must be comparable types. The maps package (Go 1.21+) adds generic utilities for cloning, iterating keys/values, and equality checks.

go

Tips

  • Always use the comma-ok idiom (v, ok := m[k]) when a zero value is a valid entry. Otherwise you cannot distinguish 'missing' from 'zero'.
  • Maps are not safe for concurrent access. Use sync.Map for concurrent reads/writes, or protect with sync.RWMutex.
  • Use map[T]struct{} for sets instead of map[T]bool — struct{} uses zero bytes of storage per entry.

Strings & String Manipulation

strings.Contains / strings.Split / strings.Builder

Go strings are immutable UTF-8 byte sequences. The strings package provides all common operations. Use strings.Builder for efficient concatenation and range for Unicode-safe iteration.

go

Tips

  • len(s) returns bytes, not characters. Use utf8.RuneCountInString(s) for the character count, or range to iterate rune by rune.
  • strings.Builder is the fastest way to build strings in a loop. It avoids the O(n^2) cost of repeated concatenation.
  • strings.Cut(s, sep) (Go 1.18+) splits a string at the first occurrence and returns before, after, found — cleaner than strings.SplitN for many use cases.

Concurrency

Goroutines & WaitGroups

go func() { }() / sync.WaitGroup

Goroutines are lightweight concurrent functions managed by the Go runtime. sync.WaitGroup coordinates completion of a group of goroutines. Use a buffered channel as a semaphore to limit concurrency.

go

Tips

  • Always call wg.Add(1) before launching the goroutine, not inside it. Otherwise the parent may call wg.Wait() before Add runs.
  • In Go 1.22+, range loop variables are per-iteration, so closures in goroutines capture the correct value without needing to pass it as an argument.
  • Use bounded concurrency (semaphore pattern) when processing large batches to avoid overwhelming system resources.

Channels & Select

ch := make(chan T) / select { case ... }

Channels are typed conduits for goroutine communication. Unbuffered channels synchronize sender and receiver. Buffered channels decouple them up to the buffer size. Select multiplexes across multiple channel operations.

go

Tips

  • Use directional channel types (chan<- for send-only, <-chan for receive-only) in function signatures to prevent misuse at compile time.
  • Only the sender should close a channel. Receiving from a closed channel returns the zero value. Sending to a closed channel panics.
  • select with a default case is non-blocking. Without default, it blocks until one case is ready. Use time.After for timeouts.

Mutex & Atomic Operations

sync.Mutex / sync.RWMutex / sync/atomic

sync.Mutex provides exclusive locking for shared state. sync.RWMutex allows concurrent reads with exclusive writes. For simple counters and flags, sync/atomic operations avoid the overhead of mutex locking.

go

Tips

  • Always use defer mu.Unlock() immediately after Lock() to prevent deadlocks from early returns or panics.
  • Use sync.RWMutex when reads vastly outnumber writes. Multiple goroutines can hold RLock simultaneously.
  • Prefer atomic.Int64 and atomic.Bool (Go 1.19+) over raw atomic.AddInt64 — the typed wrappers are harder to misuse.

Context for Cancellation & Timeouts

context.WithCancel / context.WithTimeout

The context package provides cancellation signals, deadlines, and request-scoped values that propagate across API boundaries and goroutines. Every long-running or I/O operation should accept a context.Context as its first parameter.

go

Tips

  • Always defer cancel() after creating a context with WithCancel or WithTimeout — failing to cancel leaks goroutines and timers.
  • Pass context.Context as the first parameter of every function in the call chain. Never store it in a struct.
  • Use context.WithValue only for request-scoped data (trace IDs, auth tokens) — never for function parameters or optional config.

Error Handling

Error Basics & Wrapping

errors.New / fmt.Errorf("%w", err)

Go errors are values that implement the error interface. Use fmt.Errorf with %w to wrap errors while preserving the chain. errors.Join (Go 1.20+) combines multiple errors into one.

go

Tips

  • Define sentinel errors as package-level variables (var ErrNotFound = errors.New(...)) for errors that callers need to check with errors.Is.
  • Use %w (not %v) in fmt.Errorf to wrap errors. %v loses the error chain and breaks errors.Is/As.
  • errors.Join returns nil when given no non-nil errors, making it safe to use unconditionally in validation functions.

errors.Is & errors.As

errors.Is(err, target) / errors.As(err, &target)

errors.Is checks if any error in the wrapped chain matches a sentinel value. errors.As extracts a specific error type from the chain. Both traverse the full wrap chain, including errors combined with errors.Join.

go

Tips

  • Use errors.Is for sentinel errors (known values). Use errors.As for error types (when you need the structured fields).
  • errors.As requires a pointer to the target type variable. The target must be an error interface or pointer to a concrete error type.
  • Never compare errors with == directly. Always use errors.Is, which correctly handles wrapped and joined error chains.

Custom Error Types

type MyError struct { } / func (e *MyError) Error() string { }

Custom error types carry structured data beyond a message string. Implement Unwrap() to participate in the error chain so errors.Is and errors.As can traverse through your custom type to the underlying cause.

go

Tips

  • Implement Unwrap() error to make your custom error type compatible with errors.Is and errors.As. Without it, the chain stops at your type.
  • For errors wrapping multiple causes (Go 1.20+), implement Unwrap() []error instead of Unwrap() error.
  • Use constructor functions (NewNotFound, NewBadRequest) to enforce consistent error creation and reduce boilerplate.

Common Patterns

Table-Driven Tests

go

Table-driven tests are the standard Go testing pattern. Each test case is a struct in a slice, iterated with t.Run for named subtests. This gives clear names in test output, easy-to-add cases, and parallel-safe subtests when combined with t.Parallel().

Functional Options Pattern

go

Functional options provide a clean, extensible API for configuring structs with many optional parameters. Each option is a function that mutates the struct. The constructor applies sensible defaults first, then applies each option in order. This avoids config structs, builder chains, and long parameter lists.

Graceful Shutdown

go

Graceful shutdown catches OS signals (SIGINT, SIGTERM), stops accepting new connections, and waits for in-flight requests to complete within a timeout. This prevents dropped requests during deployments and is essential for production HTTP servers.

Worker Pool with errgroup

go

errgroup (from golang.org/x/sync) manages a group of goroutines with built-in error propagation, context cancellation on first error, and concurrency limiting via SetLimit. It replaces manual WaitGroup + error channel patterns and is the standard approach for concurrent operations that can fail.

Interface-Based Dependency Injection

go

Go interfaces enable clean dependency injection without frameworks. Define small interfaces in the consumer package, accept them in constructors, and swap implementations for testing. This pattern is the foundation of testable Go architecture and avoids the need for DI containers or code generation.

Watch Out For

Nil slice vs empty slice behave differently in JSON marshaling

A nil slice (var s []int) marshals to null, while an empty slice (s := []int{}) marshals to []. Initialize with a literal or make when you need a JSON array. Use slices.Clip or make([]T, 0) if the API contract requires [].

Goroutine leaks from abandoned channels or missing context cancellation

Every goroutine must have a guaranteed exit path. Use context.WithCancel or context.WithTimeout and select on ctx.Done(). Ensure channels are closed or the goroutine checks a done signal. Use goleak in tests to detect leaked goroutines.

Shadowed variables with := in inner scopes hide outer declarations silently

Short declaration (:=) in an inner block creates a new variable even if one with the same name exists in an outer scope. The outer variable is unchanged. Use go vet -shadow or the shadow analyzer to catch this. Assign explicitly (=) when you intend to modify the outer variable.

Appending to a sub-slice can modify the original slice's underlying array

A sub-slice (s[1:3]) shares the backing array with the original. Appending to it overwrites elements beyond the sub-slice's length if capacity allows. Use the three-index slice s[1:3:3] to restrict capacity and force a new allocation on append.

Maps are not safe for concurrent reads and writes

Concurrent map access without synchronization causes a runtime panic (not just a data race). Use sync.RWMutex to protect the map, or use sync.Map for simple concurrent patterns. Run tests with -race to detect unsafe access.

Returning a nil concrete type wrapped in an interface produces a non-nil interface

An interface value is nil only when both its type and value are nil. Returning (*MyError)(nil) as an error creates a non-nil interface with a nil pointer. Always return nil directly instead of a typed nil pointer: return nil, not return (*MyError)(nil).

Master Go with Stanza

Go beyond the cheatsheet with hands-on lessons and challenges.

Dive Deeper