Go Basics

Variables, pointers, closures, defer, error handling, and the core patterns you'll use every day.

Variables Constants & iota Pointers Runes Loops Closures Defer Error handling
๐Ÿ“

Variables

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 โ€” 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

  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

โ„น๏ธ
A 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 โ€” 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 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

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

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

๐Ÿ’ก
Deferred 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 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

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

Concept Syntax Notes
Short declarationx := 42Function scope only; infers type
Multiple assignmenta, b := 1, 2Swap: a, b = b, a
Blank identifier_, err := f()Discards value, suppresses unused error
Constantconst Pi = 3.14Untyped: flexible; typed: enforced
iotaconst (A = iota; B; C)0, 1, 2 โ€” resets per const block
Bitmask iota1 << iota1, 2, 4, 8 โ€” each a distinct bit
Address of&xReturns *T
Dereference*pRead/write value at pointer
Newp := new(T)Allocates, returns pointer to zero value
Rune typevar r rune = 'A'Alias for int32, represents a code point
String โ†’ runes[]rune(s)Enables character-index access
Range stringfor i, r := range sYields byte index + decoded rune
While loopfor n < 100 { }Condition only โ€” Go's while
Infinite loopfor { }Exit with break or return
Range slicefor i, v := range sindex, value
Variadic paramfunc f(ns ...int)ns is []int inside f
Spread slicef(slice...)Expand slice as variadic args
Named returnsfunc f() (n int)Use bare return; prefer for short funcs
if initif err := f(); err != nilScopes err to the if/else block
Type switchswitch v := x.(type)Dispatches on concrete type
deferdefer f()Runs after func returns; LIFO order
panicpanic("msg")Unrecoverable state; unwinds stack
recoverr := recover()Only valid inside a deferred function
Wrap errorfmt.Errorf("ctx: %w", err)Preserves chain for errors.Is / errors.As
Check errorerrors.Is(err, ErrFoo)Traverses the full wrap chain
Extract typeerrors.As(err, &target)Sets target if type found in chain