Comprehensive Go language cheatsheet, written to be something you can actually keep open while coding. It includes core syntax, language rules, standard patterns, and essential "Go-isms".
---
title: Go (Golang) Language Cheatsheet
subtitle: Comprehensive Go language cheatsheet, written to be something you can actually keep open while coding.
author: Jon LaBelle
date: February 27, 2026
source: https://jonlabelle.com/snippets/view/markdown/go-golang-language-cheatsheet
---
Comprehensive Go language cheatsheet, written to be something you can actually keep open while coding.
It includes core syntax, language rules, standard patterns, and essential "Go-isms".
---
## Table of Contents
1. [Program Structure](#program-structure)
2. [Building & Running](#building--running)
3. [Variables & Constants](#variables--constants)
4. [Primitive Types](#primitive-types)
5. [Type Conversions & Assertions](#type-conversions--assertions)
6. [Operators](#operators)
7. [Strings](#strings)
8. [Control Flow](#control-flow)
9. [Functions](#functions)
10. [Closures](#closures)
11. [Pointers](#pointers)
12. [Arrays](#arrays)
13. [Slices](#slices)
14. [Maps](#maps)
15. [Structs](#structs)
16. [Embedding](#embedding)
17. [Methods & Receivers](#methods--receivers)
18. [Interfaces](#interfaces)
19. [Generics](#generics)
20. [Error Handling](#error-handling)
21. [`defer`, `panic`, `recover`](#defer-panic-recover)
22. [`init` Functions](#init-functions)
23. [Concurrency](#concurrency)
24. [Channels](#channels)
25. [Context](#context)
26. [Packages & Modules](#packages--modules)
27. [Visibility & Naming](#visibility--naming)
28. [Memory & Zero Values](#memory--zero-values)
29. [Testing](#testing)
30. [Common Standard Library](#common-standard-library)
31. [Example Applications](#example-applications)
32. [Common Gotchas](#common-gotchas)
33. [Idiomatic Go Rules](#idiomatic-go-rules)
---
## Program Structure
```go
package main // every executable must be in package main
import (
"fmt" // grouped imports (preferred over multiple import statements)
"os"
)
func main() { // entry point — takes no args, returns nothing
fmt.Println("Hello, Go")
os.Exit(0) // explicit exit code (rarely needed; 0 is default)
}
```
- Every executable has `package main` and `func main()`
- Imports are explicit — unused imports are a **compile error**
- Use `_` to import for side effects only: `import _ "net/http/pprof"`
---
## Building & Running
### Run Without Compiling
```bash
go run main.go # compile + run in one step (no binary produced)
go run . # run the package in the current directory
```
### Build a Binary
```bash
go build # builds binary named after module (in current dir)
go build -o myapp # specify output binary name
go build -o bin/myapp ./cmd/server # build a specific sub-package
```
### Install Globally
```bash
go install # builds and places binary in $GOPATH/bin (or $GOBIN)
go install example.com/tool@latest # install a remote tool
```
### Cross-Compilation
```bash
# Go cross-compiles with just two env vars — no extra toolchain needed
GOOS=linux GOARCH=amd64 go build -o myapp-linux
GOOS=darwin GOARCH=arm64 go build -o myapp-mac
GOOS=windows GOARCH=amd64 go build -o myapp.exe
```
Common `GOOS`/`GOARCH` combos:
| `GOOS` | `GOARCH` | Target |
| --------- | -------- | ------------------------------ |
| `linux` | `amd64` | Linux x86-64 |
| `linux` | `arm64` | Linux ARM (e.g., AWS Graviton) |
| `darwin` | `amd64` | macOS Intel |
| `darwin` | `arm64` | macOS Apple Silicon |
| `windows` | `amd64` | Windows x86-64 |
### Build Tags
```go
//go:build linux
// +build linux // old syntax (pre-1.17), still works
package mypackage
```
```bash
go build -tags "integration" # include files with //go:build integration
```
### Embed Version Info at Build Time
```go
// main.go
var version = "dev" // default value
```
```bash
go build -ldflags "-X main.version=1.2.3" -o myapp
# sets the version variable at compile time
```
### Build Flags Summary
| Flag | Purpose |
| --------------------------- | --------------------------------------- |
| `-o name` | output binary name |
| `-v` | verbose (print packages being compiled) |
| `-race` | enable race detector |
| `-ldflags "-s -w"` | strip debug info (smaller binary) |
| `-ldflags "-X pkg.Var=val"` | inject string variable at build time |
| `-tags "tag1 tag2"` | enable build tags |
| `-trimpath` | remove local file paths from binary |
### What `go build` Produces
- A **single static binary** — no runtime dependencies, no VM, no interpreter
- Includes the Go runtime and garbage collector
- Typical binary size: 5-15 MB (use `-ldflags "-s -w"` to shrink)
- Binary is ready to deploy — just copy it to the target machine
---
## Variables & Constants
### Short Declaration (inside functions only)
```go
x := 10 // type inferred as int
name := "Jon" // type inferred as string
a, b := 1, "hello" // multiple assignment
```
### Explicit Declaration (anywhere)
```go
var x int = 10 // explicit type + value
var y = 20 // type inferred
var z int // zero-initialized (z == 0)
var a, b int // multiple vars, both zero
```
### Package-Level Variables
```go
var Version = "1.0.0" // exported (uppercase)
var debug = false // unexported (lowercase)
```
### Constants
```go
const Pi = 3.14159 // untyped constant — higher precision until assigned
const Timeout = 5 * time.Second
const (
StatusOK = 200
StatusNotOK = 400
)
```
### Iota (auto-incrementing constant generator)
```go
const (
Read = 1 << iota // 1
Write // 2
Execute // 4
)
const (
Sunday = iota // 0
Monday // 1
Tuesday // 2
)
```
- Constants must be compile-time evaluable
- No `const` for slices, maps, or structs
- Untyped constants adapt to context: `const x = 5` works as int, float64, etc.
---
## Primitive Types
### Numeric
```go
int, int8, int16, int32, int64 // signed integers
uint, uint8, uint16, uint32, uint64 // unsigned integers
float32, float64 // IEEE 754 floats
complex64, complex128 // complex numbers
uintptr // pointer-sized unsigned int (unsafe)
```
> `int` and `uint` are platform-dependent: 32-bit on 32-bit systems, 64-bit on 64-bit.
### Aliases
```go
byte // alias for uint8 — used for raw data
rune // alias for int32 — represents a Unicode code point
```
### Other
```go
bool // true or false (zero value: false)
string // immutable UTF-8 byte sequence (zero value: "")
```
### Type Sizes
| Type | Size | Range |
| --------- | ------- | --------------------------- |
| `int8` | 1 byte | -128 to 127 |
| `int16` | 2 bytes | -32,768 to 32,767 |
| `int32` | 4 bytes | -2.1B to 2.1B |
| `int64` | 8 bytes | ±9.2 quintillion |
| `float32` | 4 bytes | ~7 decimal digits precision |
| `float64` | 8 bytes | ~15 decimal digits |
---
## Type Conversions & Assertions
### Type Conversion (between compatible types)
```go
i := 42
f := float64(i) // int → float64
u := uint(f) // float64 → uint
s := string(rune(65)) // int → rune → string ("A")
```
> Go has **no implicit conversions**. Even `int32` → `int64` requires explicit cast.
### String ↔ Number
```go
import "strconv"
s := strconv.Itoa(42) // int → string: "42"
i, err := strconv.Atoi("42") // string → int: 42
f, err := strconv.ParseFloat("3.14", 64) // string → float64
```
### Type Assertion (interfaces only)
```go
var x any = "hello"
s := x.(string) // panics if x is not a string
s, ok := x.(string) // safe — ok is false if wrong type
if s, ok := x.(string); ok {
fmt.Println(s) // use s safely
}
```
### Type Switch
```go
switch v := x.(type) {
case int:
fmt.Println("int:", v)
case string:
fmt.Println("string:", v)
case nil:
fmt.Println("nil")
default:
fmt.Printf("unknown: %T\n", v)
}
```
---
## Operators
### Arithmetic
```go
+ - * / % // add, subtract, multiply, divide, modulo
++ -- // increment/decrement (statement only, not expression)
```
> `i++` is a statement. You **cannot** write `x = i++`.
### Comparison
```go
== != < <= > >=
```
### Logical
```go
&& || ! // AND, OR, NOT (short-circuit evaluation)
```
### Bitwise
```go
& // AND
| // OR
^ // XOR (also unary NOT)
<< // left shift
>> // right shift
&^ // AND NOT (bit clear)
```
### Assignment
```go
= := += -= *= /= %= &= |= ^= <<= >>=
```
- ❌ No ternary operator (`? :`) — use `if/else`
- ❌ No operator overloading
---
## Strings
```go
s := "hello, world" // double quotes for interpreted strings
r := `raw \n string` // backticks for raw strings (no escape processing)
```
### Common Operations
```go
len(s) // byte length (not rune count!)
utf8.RuneCountInString(s) // actual character count
s[0] // byte at index (not rune!)
s[1:4] // substring (byte indices)
s + " suffix" // concatenation (creates new string)
```
### Iterating
```go
for i, r := range s { // r is a rune, i is byte index
fmt.Printf("%d: %c\n", i, r)
}
// to iterate bytes:
for i := 0; i < len(s); i++ {
fmt.Println(s[i])
}
```
### strings Package
```go
import "strings"
strings.Contains(s, "llo") // true
strings.HasPrefix(s, "hel") // true
strings.HasSuffix(s, "rld") // true
strings.Index(s, "lo") // 3
strings.ToUpper(s) // "HELLO, WORLD"
strings.ToLower(s) // "hello, world"
strings.TrimSpace(" hi ") // "hi"
strings.Split("a,b,c", ",") // []string{"a", "b", "c"}
strings.Join([]string{"a","b"}, "-") // "a-b"
strings.ReplaceAll(s, "l", "L") // "heLLo, worLd"
strings.Repeat("ha", 3) // "hahaha"
```
### Efficient String Building
```go
var b strings.Builder // use Builder for many concatenations
b.WriteString("hello")
b.WriteString(" world")
s := b.String() // "hello world"
```
> Strings are **immutable**. Every concatenation allocates a new string. Use `strings.Builder` or `bytes.Buffer` in loops.
---
## Control Flow
### If / Else
```go
if x > 0 {
// positive
} else if x == 0 {
// zero
} else {
// negative
}
```
With initializer (scoped to if/else block):
```go
if v, err := strconv.Atoi(s); err == nil {
fmt.Println(v)
} else {
fmt.Println("error:", err)
}
// v and err are NOT accessible here
```
---
### For (the only loop keyword)
```go
// classic C-style
for i := 0; i < 10; i++ {}
// while-style
for condition {}
// infinite loop
for {
break // exit loop
continue // skip to next iteration
}
```
### Range (iterate over collections)
```go
for i, v := range slice {} // index + value
for _, v := range slice {} // value only
for i := range slice {} // index only
for k, v := range aMap {} // key + value
for k := range aMap {} // key only
for i, r := range "hello" {} // i = byte index, r = rune
for range 5 {} // Go 1.22+: repeat 5 times
```
### Labels & Break/Continue
```go
outer:
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if j == 1 {
continue outer // skip to next outer iteration
}
if i == 2 {
break outer // exit both loops
}
}
}
```
---
### Switch
```go
switch x {
case 1:
fmt.Println("one") // no fallthrough by default (unlike C)
case 2, 3:
fmt.Println("two or three") // multiple values
fallthrough // explicit fallthrough to next case
case 4:
fmt.Println("falls here from 2 or 3")
default:
fmt.Println("other")
}
```
Expression-less switch (cleaner than if/else chains):
```go
switch {
case x > 100:
fmt.Println("big")
case x > 10:
fmt.Println("medium")
default:
fmt.Println("small")
}
```
---
## Functions
```go
func add(a int, b int) int { // basic function
return a + b
}
func add(a, b int) int { // shorthand when types match
return a + b
}
```
### Multiple Returns
```go
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
result, err := divide(10, 3)
```
### Named Returns (use judiciously — can hurt readability)
```go
func divide(a, b float64) (result float64, err error) {
if b == 0 {
err = errors.New("division by zero")
return // "naked return" — returns named values
}
result = a / b
return
}
```
### Variadic Functions
```go
func sum(nums ...int) int { // nums is []int
total := 0
for _, n := range nums {
total += n
}
return total
}
sum(1, 2, 3)
sum(nums...) // spread a slice
```
### First-Class Functions
```go
var fn func(int) int // function variable
fn = func(x int) int { return x * 2 }
fmt.Println(fn(5)) // 10
// as parameter
func apply(f func(int) int, x int) int {
return f(x)
}
```
---
## Closures
```go
func counter() func() int {
n := 0 // captured by the closure
return func() int {
n++ // mutates the captured variable
return n
}
}
c := counter()
fmt.Println(c()) // 1
fmt.Println(c()) // 2
fmt.Println(c()) // 3
```
⚠️ **Loop variable capture pitfall:**
```go
// WRONG — all goroutines share the same `i`
for i := 0; i < 5; i++ {
go func() { fmt.Println(i) }() // likely prints "5" five times
}
// CORRECT (Go <1.22) — pass as argument
for i := 0; i < 5; i++ {
go func(i int) { fmt.Println(i) }(i)
}
// Go 1.22+ — loop vars are per-iteration by default (fixed!)
```
---
## Pointers
```go
x := 42
p := &x // p is *int, points to x
fmt.Println(*p) // 42 — dereference to read
*p = 100 // modify x through the pointer
fmt.Println(x) // 100
```
### Why Use Pointers?
```go
// 1. Mutate the caller's data
func double(n *int) {
*n *= 2
}
x := 5
double(&x)
fmt.Println(x) // 10
// 2. Avoid copying large structs
func process(u *User) { /* works on original, no copy */ }
// 3. Signal "optional" values (nil means absent)
func find(id int) *User {
if id == 0 {
return nil
}
return &User{ID: id}
}
```
- ❌ No pointer arithmetic (use `unsafe` package if you must — you shouldn't)
- `new(T)` allocates zeroed `T` and returns `*T`: `p := new(int) // *int, value 0`
- Pointers to local variables are fine — Go's escape analysis handles it
---
## Arrays
```go
var a [3]int // [0, 0, 0] — zero-valued
b := [3]int{1, 2, 3} // [1, 2, 3]
c := [...]int{1, 2, 3} // compiler counts: [3]int
```
- **Fixed size** — size is part of the type: `[3]int ≠ [4]int`
- **Value types** — assignment and function args copy the entire array
- Rarely used directly; **use slices instead**
---
## Slices
A slice is a reference to a contiguous segment of an array: `(pointer, length, capacity)`.
```go
s := []int{1, 2, 3} // slice literal (no size = slice, not array)
s := make([]int, 5) // len=5, cap=5, all zeros
s := make([]int, 0, 10) // len=0, cap=10 (pre-allocate)
```
### Append
```go
s = append(s, 4) // may allocate a new backing array
s = append(s, 5, 6, 7) // append multiple
s = append(s, other...) // append another slice
```
### Slice Expressions
```go
a := []int{0, 1, 2, 3, 4}
b := a[1:3] // [1, 2] — shares backing array with a
c := a[:3] // [0, 1, 2]
d := a[2:] // [2, 3, 4]
e := a[:] // full slice (shallow copy of header, same backing array)
```
### Full Slice Expression (limit capacity)
```go
b := a[1:3:3] // [1, 2] — cap=2, prevents append from overwriting a's data
```
### Copy
```go
dst := make([]int, len(src))
n := copy(dst, src) // returns number of elements copied (min of both lens)
```
### Delete Element (order-preserving)
```go
s = append(s[:i], s[i+1:]...) // remove element at index i
s = slices.Delete(s, i, i+1) // Go 1.21+ (preferred)
```
### Nil vs Empty
```go
var s []int // nil — len=0, cap=0, s == nil is true
s := []int{} // empty — len=0, cap=0, s == nil is false
// both work with len(), cap(), append(), range
// json.Marshal: nil → "null", empty → "[]"
```
⚠️ Slices returned by sub-slicing share the same backing array. Mutating one affects the other.
---
## Maps
```go
m := map[string]int{ // map literal
"a": 1,
"b": 2,
}
m := make(map[string]int) // empty map
m := make(map[string]int, 100) // empty map with capacity hint
```
### Operations
```go
m["key"] = 42 // set
v := m["key"] // get (returns zero value if missing)
v, ok := m["key"] // comma-ok idiom — ok is false if missing
delete(m, "key") // delete (no-op if key absent)
len(m) // number of entries
```
### Iteration
```go
for k, v := range m {
fmt.Println(k, v) // order is randomized each run!
}
```
### Nil Maps
```go
var m map[string]int // nil map
_ = m["key"] // safe — returns zero value
m["key"] = 1 // PANIC — can't write to nil map
```
- **Reference type** — passing to a function shares the underlying data
- **Not thread-safe** — concurrent reads/writes cause a fatal crash. Use `sync.Map` or a `sync.RWMutex`
- Keys must be **comparable** (`==` supported): no slices, maps, or functions as keys
---
## Structs
```go
type User struct {
ID int
Name string
Email string `json:"email"` // struct tag for JSON field name
CreatedAt time.Time `json:"created_at,omitempty"` // omit if zero
}
```
### Construction
```go
u := User{ID: 1, Name: "Jon"} // named fields (preferred)
u := User{1, "Jon", "", time.Time{}} // positional (fragile, avoid)
p := &User{ID: 1} // pointer to struct
u := new(User) // *User, zero-valued
```
### Access
```go
u.Name // direct field access
p.Name // auto-dereferenced (same as (*p).Name)
```
### Anonymous Structs (useful for tests, one-off data)
```go
point := struct {
X, Y int
}{X: 1, Y: 2}
```
### Struct Comparison
```go
// Structs are comparable if all fields are comparable
a := User{ID: 1, Name: "Jon"}
b := User{ID: 1, Name: "Jon"}
fmt.Println(a == b) // true
```
---
## Embedding
Go uses **composition** instead of inheritance.
```go
type Animal struct {
Name string
}
func (a Animal) Speak() string {
return a.Name + " speaks"
}
type Dog struct {
Animal // embedded — Dog "inherits" Animal's fields and methods
Breed string
}
d := Dog{
Animal: Animal{Name: "Rex"},
Breed: "Labrador",
}
fmt.Println(d.Name) // "Rex" — promoted field
fmt.Println(d.Speak()) // "Rex speaks" — promoted method
```
- Embedding is **not inheritance** — there is no polymorphism between Dog and Animal
- Outer type can **override** promoted methods by defining its own
- You can embed **interfaces** too (useful for partial implementation / decoration)
---
## Methods & Receivers
```go
// Value receiver — operates on a copy
func (u User) FullName() string {
return u.Name
}
// Pointer receiver — can mutate the original
func (u *User) SetName(n string) {
u.Name = n
}
```
### Rules
| Use pointer receiver when... | Use value receiver when... |
| -------------------------------------------------------- | ---------------------------------- |
| Method modifies the receiver | Method only reads the receiver |
| Struct is large (avoids copy) | Struct is small (e.g., Point{X,Y}) |
| **Consistency** — if any method uses pointer, all should | Type is immutable by design |
```go
// Go auto-dereferences — you can call pointer methods on values and vice versa
u := User{Name: "Jon"}
u.SetName("Jane") // compiler takes &u automatically
```
- Methods can only be defined on types in the **same package**
- You **cannot** define methods on built-in types directly (use a named type)
---
## Interfaces
```go
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
// Interface composition
type ReadWriter interface {
Reader
Writer
}
```
### Implicit Implementation
No `implements` keyword — if a type has the required methods, it satisfies the interface.
```go
type MyReader struct{}
func (MyReader) Read(p []byte) (int, error) {
return 0, io.EOF
}
var r Reader = MyReader{} // works — MyReader satisfies Reader
```
### Empty Interface
```go
var x any // any == interface{} (alias since Go 1.18)
x = 42
x = "hello"
x = []int{1, 2} // anything can be assigned
```
### Interface Best Practices
- Keep interfaces **small** (1-2 methods)
- Define interfaces where they're **consumed**, not where they're implemented
- "Accept interfaces, return structs"
- `io.Reader`, `io.Writer`, `fmt.Stringer`, `error` — learn these well
### Nil Interface Gotcha
```go
var p *User = nil
var i any = p
fmt.Println(i == nil) // false! interface holds (*User, nil), not nil itself
```
---
## Generics
```go
func Map[T any, 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
}
// usage
doubled := Map([]int{1, 2, 3}, func(x int) int { return x * 2 })
```
### Constraints
```go
type Number interface {
~int | ~int32 | ~int64 | ~float32 | ~float64
// ~ means underlying type (allows named types like `type Age int`)
}
func Sum[T Number](nums []T) T {
var total T
for _, n := range nums {
total += n
}
return total
}
```
### Built-in Constraints
```go
any // no constraint (== interface{})
comparable // supports == and != (usable as map keys)
```
### Generic Types
```go
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) {
if len(s.items) == 0 {
var zero T
return zero, false
}
v := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return v, true
}
```
---
## Error Handling
### The Pattern
```go
result, err := doSomething()
if err != nil {
return fmt.Errorf("doSomething failed: %w", err) // wrap with context
}
```
### Creating Errors
```go
// Simple
err := errors.New("something went wrong")
// Formatted
err := fmt.Errorf("user %d not found", id)
// Wrapped (preserves chain for inspection)
err := fmt.Errorf("query failed: %w", originalErr)
```
### Custom Error Types
```go
type NotFoundError struct {
ID int
Type string
}
func (e *NotFoundError) Error() string {
return fmt.Sprintf("%s %d not found", e.Type, e.ID)
}
// Return it
return nil, &NotFoundError{ID: id, Type: "user"}
```
### Inspecting Errors
```go
// Is — checks if any error in the chain matches a target value
if errors.Is(err, os.ErrNotExist) {
fmt.Println("file not found")
}
// As — checks if any error in the chain matches a target type
var nfe *NotFoundError
if errors.As(err, &nfe) {
fmt.Println("missing:", nfe.Type, nfe.ID)
}
```
### Sentinel Errors
```go
var ErrNotFound = errors.New("not found") // package-level, exported
var errInternal = errors.New("internal") // package-level, unexported
```
> **Never** compare errors with `==` unless they're sentinel values. Use `errors.Is()`.
---
## `defer`, `panic`, `recover`
### defer
```go
f, err := os.Open("file.txt")
if err != nil {
return err
}
defer f.Close() // guaranteed to run when function returns
// Multiple defers execute in LIFO order (stack)
defer fmt.Println("first")
defer fmt.Println("second") // prints "second" then "first"
```
> Deferred calls' **arguments are evaluated immediately**, but the call happens later:
>
> ```go
> x := 1
> defer fmt.Println(x) // prints 1, not 2
> x = 2
> ```
### panic
```go
panic("something went catastrophically wrong")
panic(fmt.Sprintf("unexpected type: %T", v))
```
- Unwinds the call stack, running deferred functions
- Crashes the program if not recovered
- **Don't use for normal error handling** — only for truly unrecoverable situations
### recover
```go
func safeCall() {
defer func() {
if r := recover(); r != nil {
fmt.Println("recovered:", r)
}
}()
panic("oh no")
}
```
- `recover()` only works inside a **deferred function**
- Returns `nil` if no panic is in progress
- Common in libraries and HTTP handlers to prevent one bad request from crashing the server
---
## `init` Functions
```go
package mypackage
func init() {
// runs automatically when package is imported
// before main() executes
// used for: registering drivers, setting defaults, validation
}
```
- Each file can have **multiple** `init()` functions
- Execution order: package-level vars → `init()` → `main()`
- Avoid complex logic in `init()` — it makes testing and debugging harder
---
## Concurrency
### Goroutines
```go
go doWork() // fire and forget
go func() { // anonymous goroutine
fmt.Println("running concurrently")
}()
```
- Goroutines are **multiplexed** onto OS threads by Go's runtime scheduler
- They start with a ~8KB stack that grows dynamically
- Spawning thousands (even millions) is normal
### sync.WaitGroup (wait for goroutines to finish)
```go
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1) // increment before launching
go func(id int) {
defer wg.Done() // decrement when done
fmt.Println("worker", id)
}(i)
}
wg.Wait() // block until counter is zero
```
### sync.Mutex (protect shared data)
```go
var mu sync.Mutex
var count int
func increment() {
mu.Lock()
defer mu.Unlock()
count++
}
```
### sync.RWMutex (multiple readers, single writer)
```go
var mu sync.RWMutex
func read() int {
mu.RLock()
defer mu.RUnlock()
return count
}
```
### sync.Once (run exactly once, even across goroutines)
```go
var once sync.Once
var instance *DB
func GetDB() *DB {
once.Do(func() {
instance = connectDB()
})
return instance
}
```
⚠️ **Concurrency rules:**
- Don't communicate by sharing memory; share memory by communicating (channels)
- If you share memory, protect it with a mutex
- No automatic cancellation or cleanup for goroutines — you must handle it
---
## Channels
Channels are typed conduits for passing data between goroutines.
```go
ch := make(chan int) // unbuffered — send blocks until receiver is ready
ch := make(chan int, 10) // buffered — send blocks only when buffer is full
```
### Send / Receive
```go
ch <- 42 // send (blocks if unbuffered and no receiver)
v := <-ch // receive (blocks until a value is sent)
v, ok := <-ch // ok is false if channel is closed and empty
```
### Close
```go
close(ch) // signal that no more values will be sent
// receiving from closed channel returns zero value immediately
// sending to closed channel panics!
```
### Range Over Channel
```go
for v := range ch { // loops until channel is closed
fmt.Println(v)
}
```
### Directional Channels (used in function signatures)
```go
func produce(ch chan<- int) { ch <- 1 } // send-only
func consume(ch <-chan int) { <-ch } // receive-only
```
### Select (multiplexing channels)
```go
select {
case v := <-ch1:
fmt.Println("from ch1:", v)
case ch2 <- 42:
fmt.Println("sent to ch2")
case <-time.After(1 * time.Second):
fmt.Println("timeout")
default:
fmt.Println("no channel ready") // non-blocking if default present
}
```
### Common Patterns
```go
// Done channel (signaling completion)
done := make(chan struct{})
go func() {
defer close(done)
doWork()
}()
<-done // wait for completion
// Fan-out: launch multiple goroutines reading from one channel
// Fan-in: merge multiple channels into one
// Pipeline: chain of stages connected by channels
```
---
## Context
Used for cancellation, deadlines, and request-scoped values across API boundaries.
```go
import "context"
// Create contexts
ctx := context.Background() // root context (top-level)
ctx := context.TODO() // placeholder when unsure
ctx, cancel := context.WithCancel(parent) // manual cancellation
ctx, cancel := context.WithTimeout(parent, 5*time.Second) // auto-cancel after timeout
ctx, cancel := context.WithDeadline(parent, deadline) // auto-cancel at deadline
defer cancel() // ALWAYS call cancel to release resources
```
### Checking for Cancellation
```go
select {
case <-ctx.Done():
fmt.Println("cancelled:", ctx.Err()) // context.Canceled or DeadlineExceeded
return
case result := <-ch:
fmt.Println(result)
}
```
### Passing Context
```go
// Always pass as the first parameter, named ctx
func FetchData(ctx context.Context, url string) ([]byte, error) {
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
// ...
}
```
> **Never** store context in a struct. Pass it explicitly.
---
## Packages & Modules
### Initialize a Module
```bash
go mod init example.com/myapp # creates go.mod
go mod tidy # add missing / remove unused dependencies
go get example.com/pkg@v1.2.3 # add a dependency
go get -u ./... # update all dependencies
```
### Import
```go
import "fmt" // standard library
import "example.com/myapp/util" // local package
import m "math" // aliased import (m.Sqrt)
import . "math" // dot import — use Sqrt directly (avoid)
import _ "image/png" // side-effect import (registers PNG decoder)
```
### Project Layout (common convention)
```
myapp/
├── go.mod
├── go.sum # checksums (not a lockfile — auto-generated)
├── main.go # package main
├── internal/ # private packages (not importable outside module)
│ └── db/
├── pkg/ # public library packages (optional convention)
│ └── util/
└── cmd/ # multiple binaries
├── server/
└── cli/
```
- One module per `go.mod` = many packages
- Package name = directory name (by convention)
- `internal/` enforced by the compiler — cannot be imported from outside
---
## Visibility & Naming
```go
func PublicFunc() // exported — accessible from other packages
func privateFunc() // unexported — package-private
type PublicType struct {
ExportedField int // accessible externally
unexportedField int // only within this package
}
```
### Naming Conventions
| Thing | Convention | Example |
| -------------------- | -------------------------------- | ------------------------ |
| Package | short, lowercase, no underscores | `http`, `strconv` |
| Interface (1 method) | method name + "er" | `Reader`, `Stringer` |
| Acronyms | all caps | `URL`, `HTTP`, `ID` |
| Getters | no `Get` prefix | `Name()` not `GetName()` |
| Local vars | short | `i`, `n`, `err`, `ctx` |
---
## Memory & Zero Values
Every type has a zero value — allocated memory is always initialized.
| Type | Zero Value |
| ----------- | ----------------- |
| `int` | `0` |
| `float64` | `0.0` |
| `bool` | `false` |
| `string` | `""` |
| `pointer` | `nil` |
| `slice` | `nil` |
| `map` | `nil` |
| `channel` | `nil` |
| `interface` | `nil` |
| `struct` | all fields zeroed |
| `func` | `nil` |
### Allocation
```go
p := new(User) // allocates zeroed User, returns *User
s := make([]int, 10) // make is for slices, maps, channels only
m := make(map[string]int)
ch := make(chan int, 5)
```
> **Stack vs heap**: Go's escape analysis decides. If a value outlives the function (e.g., returned pointer), it's heap-allocated. You rarely need to think about this.
### Nil Safety
```go
var s []int
len(s) // 0 (safe)
append(s, 1) // works (safe)
for range s {} // works (safe)
s[0] // PANIC — index out of range
var m map[string]int
m["key"] // returns 0 (safe read)
m["key"] = 1 // PANIC — assignment to nil map
```
---
## Testing
### Basic Test
```go
// file: math_test.go (must end in _test.go)
package math
import "testing"
func TestAdd(t *testing.T) { // must start with Test
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 TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"positive", 1, 2, 3},
{"zero", 0, 0, 0},
{"negative", -1, -2, -3},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Add(tt.a, tt.b); got != tt.expected {
t.Errorf("got %d, want %d", got, tt.expected)
}
})
}
}
```
### Benchmarks
```go
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(1, 2)
}
}
// run: go test -bench=.
```
### Running Tests
```bash
go test ./... # all packages
go test -v ./... # verbose
go test -run TestAdd # specific test
go test -cover # coverage
go test -race # race detector (essential for concurrent code)
```
---
## Common Standard Library
```go
// Formatting
fmt.Println("hello") // print with newline
fmt.Printf("name: %s, age: %d\n", n, a) // formatted
fmt.Sprintf("id-%d", 42) // returns string
fmt.Fprintf(w, "hello") // write to io.Writer
// I/O
io.Copy(dst, src) // copy reader to writer
io.ReadAll(r) // read all bytes (Go 1.16+)
os.ReadFile("path") // read file to []byte (Go 1.16+)
os.WriteFile("path", data, 0644) // write []byte to file
// Time
time.Now() // current time
time.Since(start) // duration since start
time.Sleep(100 * time.Millisecond)
t.Format("2006-01-02 15:04:05") // Go uses this specific reference time!
time.Parse("2006-01-02", "2025-02-01")
// JSON
json.Marshal(v) // struct → []byte
json.Unmarshal(data, &v) // []byte → struct
json.NewEncoder(w).Encode(v) // stream to writer
json.NewDecoder(r).Decode(&v) // stream from reader
// HTTP
http.Get("https://example.com")
http.ListenAndServe(":8080", handler)
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "hello")
})
// Sorting
sort.Ints(s) // sort []int in place
sort.Strings(s) // sort []string in place
slices.Sort(s) // Go 1.21+ generic sort
slices.SortFunc(s, func(a, b int) int { return a - b })
// Logging
log.Println("info message")
log.Fatalf("fatal: %v", err) // logs and calls os.Exit(1)
slog.Info("msg", "key", "value") // structured logging (Go 1.21+)
```
### Printf Verbs Cheatsheet
| Verb | Description | Example Output |
| ------ | ------------------------ | ----------------- |
| `%v` | default format | `{1 Jon}` |
| `%+v` | with field names | `{ID:1 Name:Jon}` |
| `%#v` | Go syntax | `User{ID:1}` |
| `%T` | type | `main.User` |
| `%d` | decimal integer | `42` |
| `%x` | hex | `2a` |
| `%f` | float | `3.140000` |
| `%.2f` | float (2 decimal) | `3.14` |
| `%s` | string | `hello` |
| `%q` | quoted string | `"hello"` |
| `%p` | pointer | `0xc0000b4008` |
| `%w` | wrap error (Errorf only) | — |
---
## Example Applications
### CLI Tool: Word Counter
A simple CLI that counts words, lines, and characters from stdin or files.
```go
// main.go
package main
import (
"bufio"
"fmt"
"os"
"strings"
)
func count(scanner *bufio.Scanner) (lines, words, chars int) {
for scanner.Scan() {
line := scanner.Text()
lines++
words += len(strings.Fields(line))
chars += len(line) + 1 // +1 for newline
}
return
}
func main() {
var scanner *bufio.Scanner
if len(os.Args) > 1 {
// read from file
f, err := os.Open(os.Args[1])
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
defer f.Close()
scanner = bufio.NewScanner(f)
} else {
// read from stdin
scanner = bufio.NewScanner(os.Stdin)
}
lines, words, chars := count(scanner)
fmt.Printf("%8d %8d %8d\n", lines, words, chars)
}
```
```bash
# build and run
go build -o wc .
echo "hello world" | ./wc # 1 2 12
./wc main.go # counts lines/words/chars in main.go
```
---
### HTTP Server: JSON API
A minimal REST API with health check, JSON responses, and graceful shutdown.
```go
// main.go
package main
import (
"context"
"encoding/json"
"log"
"net/http"
"os"
"os/signal"
"time"
)
type StatusResponse struct {
Status string `json:"status"`
Timestamp string `json:"timestamp"`
}
type Message struct {
ID int `json:"id"`
Text string `json:"text"`
}
// respondJSON writes a JSON response with the given status code
func respondJSON(w http.ResponseWriter, status int, data any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
func main() {
mux := http.NewServeMux()
// health check
mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
respondJSON(w, http.StatusOK, StatusResponse{
Status: "ok",
Timestamp: time.Now().UTC().Format(time.RFC3339),
})
})
// list messages
messages := []Message{
{ID: 1, Text: "Hello, Go!"},
{ID: 2, Text: "Concurrency is not parallelism."},
}
mux.HandleFunc("GET /messages", func(w http.ResponseWriter, r *http.Request) {
respondJSON(w, http.StatusOK, messages)
})
// create message
mux.HandleFunc("POST /messages", func(w http.ResponseWriter, r *http.Request) {
var msg Message
if err := json.NewDecoder(r.Body).Decode(&msg); err != nil {
respondJSON(w, http.StatusBadRequest, map[string]string{
"error": "invalid JSON",
})
return
}
msg.ID = len(messages) + 1
messages = append(messages, msg)
respondJSON(w, http.StatusCreated, msg)
})
// configure server
srv := &http.Server{
Addr: ":8080",
Handler: mux,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 60 * time.Second,
}
// start server in a goroutine
go func() {
log.Printf("listening on %s", srv.Addr)
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Fatalf("server error: %v", err)
}
}()
// graceful shutdown: wait for interrupt signal
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt)
<-quit // block until SIGINT
log.Println("shutting down...")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatalf("shutdown error: %v", err)
}
log.Println("server stopped")
}
```
```bash
# build and run
go build -o api-server .
./api-server
# test endpoints (in another terminal)
curl http://localhost:8080/health
curl http://localhost:8080/messages
curl -X POST http://localhost:8080/messages \
-H "Content-Type: application/json" \
-d '{"text": "A new message"}'
```
> **Note:** The `"GET /messages"` pattern syntax requires **Go 1.22+**. For older versions, use `mux.HandleFunc("/messages", handler)` and check `r.Method` manually.
---
### Concurrent Pipeline: File Processor
Demonstrates goroutines, channels, WaitGroup, and the pipeline pattern.
```go
// main.go
package main
import (
"crypto/sha256"
"fmt"
"os"
"path/filepath"
"sync"
)
// FileHash holds a file path and its SHA-256 checksum
type FileHash struct {
Path string
Hash string
Err error
}
// walk sends file paths on a channel (producer)
func walk(root string) <-chan string {
paths := make(chan string)
go func() {
defer close(paths) // close signals no more files
filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() {
return nil
}
paths <- path
return nil
})
}()
return paths
}
// hash reads files and computes checksums (worker)
func hash(paths <-chan string, results chan<- FileHash) {
for path := range paths {
data, err := os.ReadFile(path)
if err != nil {
results <- FileHash{Path: path, Err: err}
continue
}
sum := sha256.Sum256(data)
results <- FileHash{Path: path, Hash: fmt.Sprintf("%x", sum)}
}
}
func main() {
root := "."
if len(os.Args) > 1 {
root = os.Args[1]
}
// stage 1: walk directory tree
paths := walk(root)
// stage 2: fan-out to N workers
results := make(chan FileHash)
var wg sync.WaitGroup
numWorkers := 4
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
hash(paths, results)
}()
}
// close results channel when all workers are done
go func() {
wg.Wait()
close(results)
}()
// stage 3: collect results
for r := range results {
if r.Err != nil {
fmt.Fprintf(os.Stderr, "error: %s: %v\n", r.Path, r.Err)
continue
}
fmt.Printf("%s %s\n", r.Hash[:16], r.Path)
}
}
```
```bash
go build -o hasher .
./hasher . # hash all files in current directory
./hasher /path/to/dir # hash all files in a specific directory
```
---
## Common Gotchas
| Gotcha | Explanation |
| ----------------------------- | ----------------------------------------------------------------------- |
| `nil` interface ≠ `nil` value | An interface holding a nil pointer is **not** nil itself |
| Shadowing with `:=` | Inner scope `:=` creates a new variable, doesn't reassign outer |
| Map iteration order | Randomized intentionally — don't depend on it |
| Slice sharing | Sub-slices share the backing array; use `copy` or full slice expression |
| Nil map write panics | Must `make()` a map before writing |
| JSON tags are case-sensitive | `json:"name"` must match exactly |
| Loop var capture (pre-1.22) | Goroutines in loops capture the variable, not the value |
| Goroutine leaks | Goroutines run until they return — forgotten ones leak memory |
| `defer` in loops | Defers don't run until the function exits, not the loop iteration |
| String indexing returns bytes | `"hello"[0]` is a byte, not a rune |
---
## Idiomatic Go Rules
1. **Prefer clarity over cleverness** — readable code wins
2. **Handle errors immediately** — don't defer error checking
3. **Small interfaces** — 1-2 methods; accept interfaces, return structs
4. **Composition over inheritance** — embed, don't extend
5. **One obvious way to do things** — resist abstraction for its own sake
6. **Don't stutter** — `http.Server` not `http.HTTPServer`
7. **Use `gofmt`** — non-negotiable; format your code
8. **Document exported symbols** — godoc comments start with the name
9. **Make the zero value useful** — `sync.Mutex{}` is ready to use
10. **Don't over-channel** — a mutex is fine when shared state is simpler
---
### TL;DR
Go is:
- **Simple** — small language spec, few keywords
- **Explicit** — no hidden magic, no implicit conversions
- **Fast** — compiled, statically typed, low-overhead concurrency
- **Predictable** — one way to format, one way to handle errors
It rewards discipline and punishes cleverness.