Variables
var ยท := ยท zero values
Declaration forms
var / :=
// var โ explicit type, usable at package scope var name string = "gopher" var count int // zero value: 0 var active bool // zero value: false // var block โ cleaner for multiple declarations var ( host string = "localhost" port int = 5432 tls bool ) // := short declaration โ type inferred, function scope only name := "gopher" count := 42 ratio := 3.14 // inferred as float64 // multiple assignment in one line x, y := 10, 20 a, b, c := "hello", true, 3.14 // swap without a temp variable x, y = y, x
Zero values & blank identifier
Zero values
// Every type has a zero value โ Go never leaves memory uninitialised var i int // 0 var f float64 // 0.0 var b bool // false var s string // "" (empty string) var p *int // nil var sl []int // nil (a nil slice is valid and has length 0) var m map[string]int // nil (reading is ok, writing panics) // _ discards a value โ suppresses "declared and not used" error _, err := fmt.Println("hi") _ = err // useful when you only want one value from a multi-return val, _ := strconv.Atoi("42") // ignore error
Constants & iota
const ยท typed ยท iota
const โ compile-time values
const
// Untyped โ flexible, adapts to context const Pi = 3.14159 const MaxRetries = 3 // Typed โ enforced at assignment const Timeout time.Duration = 5 * time.Second // const block const ( StatusOK = 200 StatusNotFound = 404 Version = "1.0.0" )
iota โ auto-incrementing enums
iota
// iota starts at 0 and increments per const line type Direction int const ( North Direction = iota // 0 East // 1 South // 2 West // 3 ) // Skip a value with _ const ( _ = iota // 0 โ discard KB = 1 << (10 * iota) // 1024 MB // 1048576 GB // 1073741824 )
Use
iota when you need a set of related constants that don't have meaningful numeric values themselves โ like states, directions, or flags. It removes hardcoded numbers, so inserting or reordering values never causes silent bugs from miscounted integers.
iota patterns โ bitmasks & expressions
Patterns
// Bitmask flags โ each const is a distinct bit type Permission uint const ( Read Permission = 1 << iota // 1 Write // 2 Execute // 4 ) p := Read | Write // combine flags p&Read != 0 // check if flag is set: true // iota with a formula type Weekday int const ( Sunday Weekday = iota Monday Tuesday Wednesday Thursday Friday Saturday ) fmt.Println(Wednesday) // 3
Pointers
& ยท * ยท nil Regular variable Pointer variable
x := 42 p := &x
โโโโโโโโโโโโโ โโโโโโโโโโโโโ โโโโโโโโโโโโโ
โ 42 โ โ 0xc000 โโโโโโโโโโถโ 42 โ
โโโโโโโโโโโโโ โโโโโโโโโโโโโ โโโโโโโโโโโโโ
addr: 0xc000 addr: 0xc008 addr: 0xc000
x p *p
x holds the value directly. p holds the address of x.
Changing x affects only x. *p reads or writes x through the address.
Address & dereference
* / &
x := 42 p := &x // p is *int โ holds address of x *p = 100 // dereference: sets x to 100 fmt.Println(x) // 100 // new() allocates and returns a pointer to a zero value n := new(int) // *int pointing to 0 *n = 7 // Struct fields through a pointer โ no need to write (*s).Field type Point struct{ X, Y int } pt := &Point{1, 2} pt.X = 10 // auto-dereferenced
Passing pointers โ mutate vs copy
Pass by ref
// Go passes everything by value โ pointer lets the callee mutate func increment(n *int) { *n++ } count := 5 increment(&count) fmt.Println(count) // 6 // Without the pointer, the original is unchanged func noEffect(n int) { n++ } noEffect(count) fmt.Println(count) // still 6 // Always nil-check before dereferencing var p *int if p != nil { fmt.Println(*p) }
You dereference a pointer when you need to read or write the value it points to โ not the address. Common reasons: mutating a caller's variable from inside a function, sharing a single large struct without copying it, or building linked data structures where nodes reference each other.
Runes
rune ยท UTF-8 ยท rangeA
rune is an alias for int32 representing a Unicode code point. Go source is UTF-8, and strings are byte sequences โ indexing a string gives bytes, not characters.
Bytes vs runes
UTF-8
s := "Hello, ไธ็" fmt.Println(len(s)) // 13 โ bytes, not characters fmt.Println(utf8.RuneCountInString(s)) // 9 โ Unicode code points // Indexing gives a byte (uint8) fmt.Printf("%T %v\n", s[0], s[0]) // uint8 72 // range over string decodes UTF-8 and yields runes for i, r := range s { fmt.Printf("%d: %c (%d)\n", i, r, r) } // 0: H (72) // 1: e (101) // ... 7: ไธ (19990) โ byte index 7, not character index 7 // 10: ็ (30028)
Converting between string and []rune
Conversion
s := "Hello, ไธ็" // Convert to []rune for character-index access runes := []rune(s) fmt.Println(len(runes)) // 9 fmt.Println(runes[7]) // 19990 โ 'ไธ' fmt.Println(string(runes[7])) // "ไธ" // Rune literals use single quotes var r rune = 'A' // 65 var cjk rune = 'ไธ' // 19990 // string() converts a rune (code point) back to its UTF-8 bytes fmt.Println(string(65)) // "A" fmt.Println(string([]rune{'H', 'i'})) // "Hi"
Loops
for ยท range ยท break ยท continue
for โ the only loop keyword in Go
for
// C-style for i := 0; i < 5; i++ { fmt.Print(i, " ") // 0 1 2 3 4 } // While-style โ condition only n := 1 for n < 100 { n *= 2 } fmt.Println(n) // 128 // Infinite loop โ exit with break or return for { if done { break } } // break/continue work as expected for i := 0; i < 10; i++ { if i%2 == 0 { continue } // skip evens if i > 7 { break } // stop at 8 fmt.Print(i, " ") // 1 3 5 7 }
range โ iterate over collections
range
nums := []int{10, 20, 30} // range slice โ index, value for i, v := range nums { fmt.Println(i, v) // 0 10, 1 20, 2 30 } // discard index for _, v := range nums { fmt.Println(v) } // range map โ key, value (iteration order is random) ages := map[string]int{"alice": 30, "bob": 25} for k, v := range ages { fmt.Printf("%s is %d\n", k, v) } // range string โ byte index, rune (UTF-8 decoded) for i, r := range "Go" { fmt.Printf("%d: %c\n", i, r) // 0: G, 1: o } // range channel โ reads until channel is closed ch := make(chan int, 3) ch <- 1; ch <- 2; ch <- 3; close(ch) for v := range ch { fmt.Print(v, " ") } // 1 2 3
Functions
variadic ยท first-class ยท named returns
Variadic functions & first-class values
func
// Variadic โ ...T collects extra args into a slice func sum(nums ...int) int { total := 0 for _, n := range nums { total += n } return total } sum(1, 2, 3) // 6 sum(1, 2, 3, 4, 5) // 15 s := []int{1, 2, 3} sum(s...) // spread a slice with ... // Functions are first-class โ assignable, passable, returnable add := func(a, b int) int { return a + b } apply := func(f func(int, int) int, x, y int) int { return f(x, y) } apply(add, 3, 4) // 7
Multiple returns & named return values
returns
// Multiple returns โ idiomatic (value, error) pattern func divide(a, b float64) (float64, error) { if b == 0 { return 0, fmt.Errorf("division by zero") } return a / b, nil } result, err := divide(10, 2) if err != nil { log.Fatal(err) } // Named returns โ named in signature, bare return fills them // Good for short functions; avoid in longer ones (hurts readability) func minMax(a []int) (min, max int) { min, max = a[0], a[0] for _, v := range a[1:] { if v < min { min = v } if v > max { max = v } } return // bare return }
Closures
capture ยท factory ยท gotcha
Closures capture variables by reference
Closure
// The inner function closes over `count` in the outer scope func makeCounter() func() int { count := 0 return func() int { count++ return count } } counter := makeCounter() fmt.Println(counter()) // 1 fmt.Println(counter()) // 2 fmt.Println(counter()) // 3 other := makeCounter() // independent counter, own `count` fmt.Println(other()) // 1
Loop variable capture gotcha
Gotcha
// Pre-Go 1.22: all closures capture the SAME loop variable funcs := make([]func(), 3) for i := 0; i < 3; i++ { funcs[i] = func() { fmt.Println(i) } } funcs[0]() // 3 โ surprise! i is 3 by the time this runs funcs[1]() // 3 funcs[2]() // 3 // Fix: shadow the variable inside the loop body for i := 0; i < 3; i++ { i := i // new variable per iteration funcs[i] = func() { fmt.Println(i) } } funcs[0]() // 0 funcs[1]() // 1 funcs[2]() // 2 // Go 1.22+ fixes this automatically โ each iteration gets its own i
If / Switch
init ยท type switch ยท fallthrough
if with init statement
if
// Init statement scopes the variable to the if/else block if err := doWork(); err != nil { fmt.Println("error:", err) return } // err is not in scope here // Works with any assignment if v, ok := cache[key]; ok { return v }
switch patterns
switch
// No expression โ acts as if-else chain switch { case score >= 90: fmt.Println("A") case score >= 80: fmt.Println("B") default: fmt.Println("C") } // Multiple values per case switch day { case "Sat", "Sun": fmt.Println("weekend") default: fmt.Println("weekday") }
Type switch โ dispatch on interface 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) case []int: return fmt.Sprintf("[]int of len %d", len(v)) default: return fmt.Sprintf("unknown: %T", v) } } describe(42) // "int: 42" describe("hello") // "string: \"hello\"" describe([]int{1,2}) // "[]int of len 2" // fallthrough โ rare; forces next case to execute unconditionally switch n { case 1: fmt.Println("one") fallthrough case 2: fmt.Println("one or two") }
Defer
LIFO ยท args evaluated ยท cleanupDeferred calls run after the surrounding function returns, in LIFO order. Arguments are evaluated when defer is called, not when the deferred function runs.
Defer for cleanup & LIFO ordering
defer
// Classic use: guarantee resource cleanup regardless of return path func readFile(path string) (string, error) { f, err := os.Open(path) if err != nil { return "", err } defer f.Close() // runs when readFile returns // ... read file ... return content, nil } // Multiple defers โ LIFO (last-in, first-out) defer fmt.Println("first") defer fmt.Println("second") defer fmt.Println("third") // Output: third, second, first
Args evaluated immediately, not at call time
Gotcha
// The argument i is evaluated NOW (when defer is called) for i := 0; i < 3; i++ { defer fmt.Println(i) } // Prints: 2, 1, 0 โ values captured at each defer call // Deferred func can read named return values (and modify them) func double(n int) (result int) { defer func() { result *= 2 }() result = n return // returns n*2 } double(5) // 10
Avoid
defer inside a tight loop. Each call is registered separately and none of them run until the enclosing function returns โ not at the end of each iteration. This leaks resources and adds overhead proportional to loop count.Panic & Recover
panic ยท recover ยท deferredPanic is for unrecoverable programmer errors, not expected runtime conditions. Prefer returning errors. Only use panic for truly impossible states (invariant violations, failed assertions).
panic โ unwind the stack
panic
// panic stops normal execution and unwinds, // running deferred functions along the way func mustPositive(n int) int { if n <= 0 { panic(fmt.Sprintf( "expected positive, got %d", n)) } return n } // Typical in init paths where failure is fatal db := mustConnect(dsn) // panics on failure cfg := mustLoadConfig() // panics on failure
recover โ catch a panic in defer
recover
// recover() only works inside a deferred function 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 // panics if b == 0 } v, err := safeDiv(10, 0) // v=0 err="recovered: runtime error: integer divide by zero"
Error Handling
errors ยท wrapping ยท sentinel ยท custom
Creating & wrapping errors
errors
import ( "errors" "fmt" ) // errors.New โ simple, no formatting var ErrNotFound = errors.New("not found") // fmt.Errorf โ with context; %w wraps for unwrapping later func getUser(id int) (*User, error) { u, ok := db[id] if !ok { return nil, fmt.Errorf("getUser %d: %w", id, ErrNotFound) } return u, nil } // errors.Is โ checks the chain for a target error _, err := getUser(99) errors.Is(err, ErrNotFound) // true โ works even when wrapped // errors.As โ extracts a specific error type from the chain var ne *NetworkError if errors.As(err, &ne) { fmt.Println("network error:", ne.Code()) }
Custom error types
Custom
// Implement the error interface: Error() string type ValidationError struct { Field string Message string } func (e *ValidationError) Error() string { return fmt.Sprintf("validation: %s โ %s", e.Field, e.Message) } func validate(age int) error { if age < 0 { return &ValidationError{Field: "age", Message: "must be non-negative"} } return nil } err := validate(-1) fmt.Println(err) // "validation: age โ must be non-negative" var ve *ValidationError if errors.As(err, &ve) { fmt.Println(ve.Field) // "age" }
Quick Reference
Syntax cheat-sheet| Concept | Syntax | Notes |
|---|---|---|
| Short declaration | x := 42 | Function scope only; infers type |
| Multiple assignment | a, b := 1, 2 | Swap: a, b = b, a |
| Blank identifier | _, err := f() | Discards value, suppresses unused error |
| Constant | const Pi = 3.14 | Untyped: flexible; typed: enforced |
| iota | const (A = iota; B; C) | 0, 1, 2 โ resets per const block |
| Bitmask iota | 1 << iota | 1, 2, 4, 8 โ each a distinct bit |
| Address of | &x | Returns *T |
| Dereference | *p | Read/write value at pointer |
| New | p := new(T) | Allocates, returns pointer to zero value |
| Rune type | var r rune = 'A' | Alias for int32, represents a code point |
| String โ runes | []rune(s) | Enables character-index access |
| Range string | for i, r := range s | Yields byte index + decoded rune |
| While loop | for n < 100 { } | Condition only โ Go's while |
| Infinite loop | for { } | Exit with break or return |
| Range slice | for i, v := range s | index, value |
| Variadic param | func f(ns ...int) | ns is []int inside f |
| Spread slice | f(slice...) | Expand slice as variadic args |
| Named returns | func f() (n int) | Use bare return; prefer for short funcs |
| if init | if err := f(); err != nil | Scopes err to the if/else block |
| Type switch | switch v := x.(type) | Dispatches on concrete type |
| defer | defer f() | Runs after func returns; LIFO order |
| panic | panic("msg") | Unrecoverable state; unwinds stack |
| recover | r := recover() | Only valid inside a deferred function |
| Wrap error | fmt.Errorf("ctx: %w", err) | Preserves chain for errors.Is / errors.As |
| Check error | errors.Is(err, ErrFoo) | Traverses the full wrap chain |
| Extract type | errors.As(err, &target) | Sets target if type found in chain |