Toolmingo
Guides14 min read

Go (Golang) Cheat Sheet: Syntax, Concurrency & Patterns

A complete Go cheat sheet — variables, types, functions, structs, interfaces, goroutines, channels, error handling, testing, and common patterns with copy-ready examples.

Go (Golang) is a statically typed, compiled language designed for simplicity and performance. This cheat sheet covers the full Go language — from variables to concurrency — with copy-ready code for everyday development.

Quick reference

The 25 patterns that cover 95% of everyday Go development.

Pattern Example
Declare variable x := 42
Typed declaration var x int = 42
Multiple return func f() (int, error)
Named return func f() (n int, err error)
Variadic function func sum(nums ...int)
Struct literal p := Point{X: 1, Y: 2}
Struct pointer p := &Point{X: 1}
Embed struct type Dog struct { Animal }
Interface impl implicit — no implements keyword
Type assertion v, ok := i.(string)
Type switch switch v := i.(type)
Goroutine go f()
Buffered channel ch := make(chan int, 10)
Select select { case v := <-ch: }
WaitGroup wg.Add(1); defer wg.Done()
Mutex mu.Lock(); defer mu.Unlock()
Error wrap fmt.Errorf("load: %w", err)
Error unwrap errors.Is(err, ErrNotFound)
Defer defer f.Close()
Slice append s = append(s, v)
Map literal m := map[string]int{"a": 1}
Range loop for i, v := range slice
Context cancel ctx, cancel := context.WithCancel(ctx)
HTTP handler http.HandleFunc("/", handler)
Test function func TestFoo(t *testing.T)

Variables and types

// Short declaration (inside functions)
x := 42
name := "Alice"
ok := true

// Explicit declaration
var x int = 42
var name string

// Multiple variables
a, b := 1, 2
a, b = b, a  // swap

// Constants
const Pi = 3.14159
const (
    KB = 1024
    MB = KB * 1024
)

// iota for enums
type Direction int
const (
    North Direction = iota  // 0
    East                    // 1
    South                   // 2
    West                    // 3
)

Basic types

Type Zero value Notes
bool false
int, int8, int16, int32, int64 0 int is platform-sized
uint, uint8uint64 0 uint8 = byte
float32, float64 0 prefer float64
complex64, complex128 0+0i
string "" immutable UTF-8 bytes
rune 0 alias for int32, one Unicode code point
byte 0 alias for uint8

Strings and runes

s := "Hello, 世界"

// Length in bytes (not characters!)
len(s)  // 13

// Rune count (Unicode characters)
import "unicode/utf8"
utf8.RuneCountInString(s)  // 9

// Iterate characters (runes)
for i, r := range s {
    fmt.Printf("%d: %c\n", i, r)
}

// Convert
bs := []byte(s)    // string → []byte
rs := []rune(s)    // string → []rune
s2 := string(bs)   // []byte → string

// Strings package
import "strings"
strings.ToUpper(s)
strings.ToLower(s)
strings.TrimSpace(s)
strings.HasPrefix(s, "He")
strings.HasSuffix(s, "界")
strings.Contains(s, "世")
strings.Replace(s, "Hello", "Hi", 1)
strings.ReplaceAll(s, "l", "L")
strings.Split(s, ",")
strings.Join(parts, ", ")
strings.Count(s, "l")
strings.Index(s, "世")  // byte offset

// Build strings efficiently
var b strings.Builder
for i := 0; i < 5; i++ {
    fmt.Fprintf(&b, "item %d\n", i)
}
result := b.String()

Slices and arrays

// Array (fixed size, rarely used directly)
var a [3]int        // [0 0 0]
a := [3]int{1,2,3}
a := [...]int{1,2,3}  // compiler counts

// Slice (dynamic)
var s []int              // nil slice
s := []int{1, 2, 3}
s := make([]int, 5)      // len=5, cap=5
s := make([]int, 0, 10)  // len=0, cap=10

// Operations
s = append(s, 4)
s = append(s, 5, 6, 7)
s = append(s, other...)  // spread

// Slicing
s[1:3]   // elements at index 1,2
s[:3]    // first 3
s[2:]    // from index 2 to end
s[:]     // full copy

// Copy (avoids aliasing)
dst := make([]int, len(src))
copy(dst, src)

// Delete element at index i (order preserved)
s = append(s[:i], s[i+1:]...)

// Delete element at index i (order not preserved — fast)
s[i] = s[len(s)-1]
s = s[:len(s)-1]

// 2D slice
matrix := [][]int{
    {1, 2, 3},
    {4, 5, 6},
}

// Sort
import "sort"
sort.Ints(s)
sort.Strings(strs)
sort.Slice(items, func(i, j int) bool {
    return items[i].Name < items[j].Name
})

Maps

// Create
m := map[string]int{}
m := map[string]int{"a": 1, "b": 2}
m := make(map[string]int)

// Read (safe — returns zero value if missing)
v := m["key"]

// Check existence
v, ok := m["key"]
if !ok {
    // key not found
}

// Write / update
m["key"] = 42

// Delete
delete(m, "key")

// Iterate (order not guaranteed)
for k, v := range m {
    fmt.Printf("%s: %d\n", k, v)
}

// Sorted iteration
import "sort"
keys := make([]string, 0, len(m))
for k := range m {
    keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
    fmt.Println(k, m[k])
}

// Map of slices
groups := map[string][]string{}
groups["admin"] = append(groups["admin"], "alice")

Functions

// Basic function
func add(a, b int) int {
    return a + b
}

// Multiple return values
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

// Named return values
func minMax(a, b int) (min, max int) {
    if a < b {
        return a, b
    }
    return b, a
}

// Variadic function
func sum(nums ...int) int {
    total := 0
    for _, n := range nums {
        total += n
    }
    return total
}
sum(1, 2, 3)
sum(nums...)  // spread slice

// First-class functions
add := func(a, b int) int { return a + b }
apply := func(f func(int, int) int, a, b int) int {
    return f(a, b)
}

// Closures
func counter() func() int {
    n := 0
    return func() int {
        n++
        return n
    }
}
c := counter()
c()  // 1
c()  // 2

// Defer (runs when function returns, LIFO order)
func readFile(path string) error {
    f, err := os.Open(path)
    if err != nil {
        return err
    }
    defer f.Close()  // guaranteed cleanup
    // ... use f
    return nil
}

Structs and methods

// Struct definition
type Point struct {
    X, Y float64
}

// Struct literal
p := Point{X: 1.0, Y: 2.0}
p := Point{1.0, 2.0}  // positional (fragile, avoid)
p := &Point{X: 1.0}   // pointer

// Field access
p.X = 3.0
fmt.Println(p.Y)

// Method with value receiver (gets a copy)
func (p Point) Distance() float64 {
    return math.Sqrt(p.X*p.X + p.Y*p.Y)
}

// Method with pointer receiver (can mutate)
func (p *Point) Scale(factor float64) {
    p.X *= factor
    p.Y *= factor
}
p.Scale(2)  // Go auto-dereferences: (&p).Scale(2)

// Embedding (composition over inheritance)
type Animal struct {
    Name string
}
func (a Animal) Speak() string { return a.Name }

type Dog struct {
    Animal          // embedded
    Breed string
}
d := Dog{Animal: Animal{Name: "Rex"}, Breed: "Husky"}
d.Speak()  // promoted method

// Anonymous struct
config := struct {
    Host string
    Port int
}{"localhost", 8080}

Interfaces

// Interface definition
type Shape interface {
    Area() float64
    Perimeter() float64
}

// Implicit implementation (no "implements" keyword)
type Rectangle struct{ W, H float64 }
func (r Rectangle) Area() float64      { return r.W * r.H }
func (r Rectangle) Perimeter() float64 { return 2 * (r.W + r.H) }

type Circle struct{ R float64 }
func (c Circle) Area() float64      { return math.Pi * c.R * c.R }
func (c Circle) Perimeter() float64 { return 2 * math.Pi * c.R }

// Use the interface
func printInfo(s Shape) {
    fmt.Printf("Area: %.2f, Perimeter: %.2f\n", s.Area(), s.Perimeter())
}
printInfo(Rectangle{3, 4})
printInfo(Circle{5})

// Empty interface (any value)
func printAny(v interface{}) { fmt.Println(v) }
// or with generics: func printAny[T any](v T)

// Type assertion
var i interface{} = "hello"
s, ok := i.(string)  // safe: ok=false instead of panic
s := i.(string)      // unsafe: panics if wrong type

// Type switch
func describe(i interface{}) string {
    switch v := i.(type) {
    case int:
        return fmt.Sprintf("int: %d", v)
    case string:
        return fmt.Sprintf("string: %q", v)
    case bool:
        return fmt.Sprintf("bool: %v", v)
    default:
        return fmt.Sprintf("unknown: %T", v)
    }
}

// Common small interfaces
type Reader interface { Read(p []byte) (n int, err error) }
type Writer interface { Write(p []byte) (n int, err error) }
type Stringer interface { String() string }

Error handling

// Errors are values — return them, don't throw
func openFile(path string) (*os.File, error) {
    f, err := os.Open(path)
    if err != nil {
        return nil, fmt.Errorf("openFile %s: %w", path, err)
    }
    return f, nil
}

// Wrapping and unwrapping
var ErrNotFound = errors.New("not found")

err := fmt.Errorf("load user: %w", ErrNotFound)
errors.Is(err, ErrNotFound)  // true — unwraps chain

// Custom error type
type ValidationError struct {
    Field   string
    Message string
}
func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation %s: %s", e.Field, e.Message)
}

var ve *ValidationError
errors.As(err, &ve)  // unwrap to custom type

// Sentinel errors
var (
    ErrNotFound   = errors.New("not found")
    ErrPermission = errors.New("permission denied")
)

// Panic / recover (use only for truly unrecoverable situations)
func safeDiv(a, b int) (result int, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("recovered: %v", r)
        }
    }()
    return a / b, nil
}

Goroutines and channels

// Goroutine — lightweight thread
go func() {
    fmt.Println("running concurrently")
}()

// Unbuffered channel (synchronous handoff)
ch := make(chan int)
go func() { ch <- 42 }()
v := <-ch  // blocks until value arrives

// Buffered channel (non-blocking up to capacity)
ch := make(chan int, 5)
ch <- 1  // doesn't block if buffer not full

// Close and range
ch := make(chan int, 3)
ch <- 1; ch <- 2; ch <- 3
close(ch)
for v := range ch {  // reads until closed
    fmt.Println(v)
}

// Select — non-blocking multiplex
select {
case v := <-ch1:
    fmt.Println("from ch1:", v)
case v := <-ch2:
    fmt.Println("from ch2:", v)
case <-time.After(time.Second):
    fmt.Println("timeout")
default:
    fmt.Println("no channel ready")
}

// WaitGroup — wait for N goroutines
var wg sync.WaitGroup
for _, url := range urls {
    wg.Add(1)
    go func(u string) {
        defer wg.Done()
        fetch(u)
    }(url)
}
wg.Wait()

// Mutex — protect shared state
type SafeCounter struct {
    mu    sync.Mutex
    count int
}
func (c *SafeCounter) Inc() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.count++
}

// Once — run exactly once
var once sync.Once
once.Do(func() { initDB() })

// Semaphore pattern — limit concurrency
sem := make(chan struct{}, 10)  // max 10 concurrent
for _, item := range items {
    sem <- struct{}{}
    go func(i Item) {
        defer func() { <-sem }()
        process(i)
    }(item)
}

Context

import "context"

// Create contexts
ctx := context.Background()         // root context
ctx := context.TODO()               // placeholder

// Cancel
ctx, cancel := context.WithCancel(ctx)
defer cancel()  // always call cancel to free resources

// Deadline / timeout
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()

ctx, cancel := context.WithDeadline(ctx, time.Now().Add(5*time.Second))
defer cancel()

// Values (use sparingly — for request-scoped data only)
type keyType string
const userKey keyType = "user"
ctx = context.WithValue(ctx, userKey, "alice")
user := ctx.Value(userKey).(string)

// Check cancellation
select {
case <-ctx.Done():
    return ctx.Err()  // context.Canceled or context.DeadlineExceeded
default:
    // continue
}

// Pass context to HTTP request
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
resp, err := http.DefaultClient.Do(req)

HTTP server and client

// Simple HTTP server
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusOK)
    json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
})
http.ListenAndServe(":8080", nil)

// Custom mux (Go 1.22+ path patterns)
mux := http.NewServeMux()
mux.HandleFunc("GET /users/{id}", getUser)
mux.HandleFunc("POST /users", createUser)
srv := &http.Server{
    Addr:         ":8080",
    Handler:      mux,
    ReadTimeout:  5 * time.Second,
    WriteTimeout: 10 * time.Second,
    IdleTimeout:  120 * time.Second,
}
srv.ListenAndServe()

// Read request body
func handler(w http.ResponseWriter, r *http.Request) {
    var payload struct {
        Name string `json:"name"`
    }
    if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    // ...
}

// HTTP client with timeout
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Get("https://api.example.com/data")
if err != nil {
    return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
    return fmt.Errorf("unexpected status: %d", resp.StatusCode)
}
var data MyStruct
json.NewDecoder(resp.Body).Decode(&data)

JSON

import "encoding/json"

// Struct tags
type User struct {
    ID       int    `json:"id"`
    Name     string `json:"name"`
    Email    string `json:"email,omitempty"`  // omit if empty
    Password string `json:"-"`                // never marshal
}

// Marshal (struct → JSON)
u := User{ID: 1, Name: "Alice", Email: "alice@example.com"}
data, err := json.Marshal(u)
// → {"id":1,"name":"Alice","email":"alice@example.com"}

// Pretty print
data, err := json.MarshalIndent(u, "", "  ")

// Unmarshal (JSON → struct)
var u User
err := json.Unmarshal(data, &u)

// Streaming (more efficient for HTTP)
json.NewEncoder(w).Encode(u)    // write to io.Writer
json.NewDecoder(r.Body).Decode(&u)  // read from io.Reader

// Dynamic JSON with map
var raw map[string]interface{}
json.Unmarshal(data, &raw)
name := raw["name"].(string)

// Or use json.RawMessage to defer parsing
type Response struct {
    Status string          `json:"status"`
    Data   json.RawMessage `json:"data"`
}

Testing

// Basic test (file must end in _test.go)
package mypackage

import "testing"

func TestAdd(t *testing.T) {
    got := Add(2, 3)
    want := 5
    if got != want {
        t.Errorf("Add(2,3) = %d; want %d", got, want)
    }
}

// Table-driven tests (idiomatic Go)
func TestDivide(t *testing.T) {
    tests := []struct {
        name    string
        a, b    float64
        want    float64
        wantErr bool
    }{
        {"normal", 10, 2, 5, false},
        {"divide by zero", 10, 0, 0, true},
        {"negative", -6, 2, -3, false},
    }
    for _, tc := range tests {
        t.Run(tc.name, func(t *testing.T) {
            got, err := Divide(tc.a, tc.b)
            if (err != nil) != tc.wantErr {
                t.Fatalf("err = %v; wantErr %v", err, tc.wantErr)
            }
            if got != tc.want {
                t.Errorf("got %v; want %v", got, tc.want)
            }
        })
    }
}

// Benchmark
func BenchmarkAdd(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Add(2, 3)
    }
}

// Run tests
// go test ./...              — all packages
// go test -v ./...           — verbose
// go test -run TestAdd       — specific test
// go test -bench=. -benchmem — benchmarks
// go test -race ./...        — race detector
// go test -cover ./...       — coverage

Generics (Go 1.18+)

// Generic function
func Map[T, U any](s []T, f func(T) U) []U {
    result := make([]U, len(s))
    for i, v := range s {
        result[i] = f(v)
    }
    return result
}
doubled := Map([]int{1, 2, 3}, func(n int) int { return n * 2 })

// Type constraints
type Number interface {
    ~int | ~int64 | ~float64
}
func Sum[T Number](nums []T) T {
    var total T
    for _, n := range nums {
        total += n
    }
    return total
}

// Generic struct
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
}

// Common constraint interfaces (golang.org/x/exp/constraints)
type Ordered interface {
    Integer | Float | ~string
}

Common mistakes

Mistake Why it hurts Fix
for _, v := range slice capturing v in goroutine All goroutines share same v go func(v T) { ... }(v) — pass as parameter
Slice aliasing after append Original and copy may share backing array copy(dst, src) for independent slices
Nil map write assignment to entry in nil map panic m := make(map[K]V) before writing
Goroutine leak Goroutine blocks forever on channel nobody reads Always use context cancellation or close channels
errors.Is fails on wrapped errors with == Direct comparison doesn't unwrap Use errors.Is / errors.As
Ignoring error return Silent failure, data corruption Always check if err != nil
http.Response.Body not closed Connection leak, file descriptor exhaustion defer resp.Body.Close() immediately after nil check

6 frequently asked questions

Is Go object-oriented? Go is not class-based OOP. It uses structs + methods + interfaces for composition. There's no inheritance — only embedding. The interface system is implicit: any type that has the required methods satisfies an interface automatically.

When should I use a pointer receiver vs value receiver? Use a pointer receiver when the method modifies the struct (*T), or when the struct is large (avoid copying). Use a value receiver for small read-only structs. Be consistent: if any method on a type has a pointer receiver, all methods should.

How do goroutines differ from threads? Goroutines are multiplexed onto OS threads by the Go scheduler. They start with a small stack (~2 KB, grows as needed), so you can run hundreds of thousands concurrently. OS threads are fixed-size (~1–8 MB each). Goroutines are not OS threads — they're Go's user-space concurrency primitive.

What is the difference between nil channel operations and closed channel operations? Sending to a nil channel blocks forever. Receiving from a nil channel blocks forever. Sending to a closed channel panics. Receiving from a closed channel returns the zero value immediately. Use v, ok := <-ch to detect a closed channel (ok == false).

How does Go handle dependencies? Go modules (go.mod, go.sum) are the standard since Go 1.11. go get adds dependencies, go mod tidy removes unused ones. The module proxy caches versions. Use semantic versioning — major version changes require a new module path (e.g., v2).

When should I use context.TODO() vs context.Background()? context.Background() is the root context for your main function or server startup — it never cancels. context.TODO() signals that you haven't decided what context to use yet and plan to refactor. Linters can flag TODO to remind you to wire up proper context propagation.

Related tools

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools