⚑ Chapter 5 of 20

Functions

Functions are first-class citizens in Go. Master every function form, understand how Go manages the call stack, and learn how closures and defer work under the hood.

⏱ β€”πŸ“Š Beginner–Advanced

🎯 Learning Objectives

βœ… After this chapter you will:
  • Write functions with any combination of parameters and return values
  • Use named return values effectively (and know when NOT to)
  • Write variadic functions and call them correctly
  • Create closures and understand how they capture variables
  • Implement higher-order functions (map, filter, reduce patterns)
  • Use defer correctly and explain its execution order
  • Understand the call stack and stack frames
  • Write recursive functions with proper base cases

πŸ“ Function Syntax

Functions are declared with the func keyword. In Go, parameter types come after parameter names (unlike C/Java).

go
package main

import "fmt"

// Basic function: one parameter, one return
func double(x int) int {
    return x * 2
}

// Multiple parameters of the same type β€” type listed once
func add(x, y int) int {   // shorthand: x int, y int β†’ x, y int
    return x + y
}

// Multiple parameters of different types
func describe(name string, age int, active bool) string {
    return fmt.Sprintf("%s (%d) active=%v", name, age, active)
}

// No return value
func greet(name string) {
    fmt.Printf("Hello, %s!\n", name)
}

// Multiple return values β€” Go's most powerful feature
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, fmt.Errorf("cannot divide %v by zero", a)
    }
    return a / b, nil
}

func main() {
    fmt.Println(double(5))          // 10
    fmt.Println(add(3, 4))          // 7
    fmt.Println(describe("Alice", 30, true))
    greet("Bob")

    result, err := divide(10, 3)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    fmt.Printf("10/3 = %.4f\n", result)  // 10/3 = 3.3333

    _, err2 := divide(5, 0)
    fmt.Println("Error:", err2)  // Error: cannot divide 5 by zero
}
βœ… Output
10 7 Alice (30) active=true Hello, Bob! 10/3 = 3.3333 Error: cannot divide 5 by zero

🏷️ Named Return Values

Go allows naming the return values. Named returns create variables that are initialised to their zero values and returned by a bare return.

go
package main

import (
    "fmt"
    "math"
)

// Named returns: quotient and remainder are pre-declared
func divmod(a, b int) (quotient, remainder int) {
    quotient = a / b
    remainder = a % b
    return  // bare return β€” returns quotient, remainder
}

// Named returns help with defer + error handling
func readConfig(path string) (config map[string]string, err error) {
    config = make(map[string]string)

    defer func() {
        // Named return 'err' is accessible in defer
        if err != nil {
            config = nil  // clean up on error
        }
    }()

    // Simulate reading a file
    if path == "" {
        err = fmt.Errorf("empty path")
        return  // bare return: returns nil config and the error
    }

    config["debug"] = "false"
    config["port"] = "8080"
    return  // bare return: returns the populated config, nil err
}

// Named returns also serve as documentation
func minMax(nums []float64) (min, max float64) {
    if len(nums) == 0 {
        return 0, 0
    }
    min, max = nums[0], nums[0]
    for _, n := range nums[1:] {
        min = math.Min(min, n)
        max = math.Max(max, n)
    }
    return  // implicitly returns min, max
}

func main() {
    q, r := divmod(17, 5)
    fmt.Printf("17/5 = %d remainder %d\n", q, r)  // 3 remainder 2

    cfg, err := readConfig("config.yaml")
    fmt.Println("config:", cfg, "err:", err)

    cfg2, err2 := readConfig("")
    fmt.Println("config:", cfg2, "err:", err2)

    mn, mx := minMax([]float64{3.1, 1.4, 5.9, 2.6, 5.3})
    fmt.Printf("min=%.1f max=%.1f\n", mn, mx)
}
βœ… Output
17/5 = 3 remainder 2 config: map[debug:false port:8080] err: <nil> config: map[] err: empty path min=1.4 max=5.9
⚠️ Named Returns: Use Sparingly

Named returns improve readability when the return values need documentation. But bare return in long functions can hide what's being returned. The Go community generally recommends: use named returns only when they genuinely help with defer-based cleanup or short, obvious functions.

πŸ”’ Variadic Functions

A variadic function accepts zero or more arguments of the same type, collected into a slice.

go
package main

import "fmt"

// nums is a []int inside the function
func sum(nums ...int) int {
    total := 0
    for _, n := range nums {
        total += n
    }
    return total
}

// Can have regular params before variadic
func logMessage(level string, args ...interface{}) {
    fmt.Printf("[%s] ", level)
    fmt.Println(args...)
}

// Passing a slice to a variadic function with ...
func main() {
    fmt.Println(sum())          // 0 (zero args is valid)
    fmt.Println(sum(1))         // 1
    fmt.Println(sum(1, 2, 3))   // 6
    fmt.Println(sum(1, 2, 3, 4, 5)) // 15

    // Pass a slice with ... operator β€” "spread" the slice
    nums := []int{10, 20, 30}
    fmt.Println(sum(nums...))  // 60

    // Modify the slice inside the function β€” affects original!
    // Variadic args share the same memory as the passed slice
    modify := func(nums ...int) {
        nums[0] = 999
    }
    s := []int{1, 2, 3}
    modify(s...)
    fmt.Println(s)  // [999 2 3] β€” modified!

    logMessage("INFO", "Server started on port", 8080)
    logMessage("ERROR", "connection failed:", "timeout")

    // The classic: fmt.Println is variadic
    fmt.Println("a", "b", "c")  // a b c
}
βœ… Output
0 1 6 15 60 [999 2 3] [INFO] Server started on port 8080 [ERROR] connection failed: timeout a b c

πŸ”€ Anonymous Functions

Functions can be defined inline without a name. They are values that can be assigned to variables, passed as arguments, and returned from functions.

go
package main

import (
    "fmt"
    "sort"
)

func main() {
    // Assign to variable
    double := func(x int) int {
        return x * 2
    }
    fmt.Println(double(5))  // 10

    // Immediately Invoked Function Expression (IIFE)
    result := func(a, b int) int {
        return a + b
    }(3, 4)   // ← called immediately with args 3, 4
    fmt.Println(result)  // 7

    // Pass as argument
    apply := func(x int, fn func(int) int) int {
        return fn(x)
    }
    fmt.Println(apply(10, double))  // 20

    // Use in sort.Slice β€” the custom comparator is an anonymous func
    people := []struct{ name string; age int }{
        {"Charlie", 30},
        {"Alice", 25},
        {"Bob", 35},
    }
    sort.Slice(people, func(i, j int) bool {
        return people[i].age < people[j].age  // sort by age ascending
    })
    for _, p := range people {
        fmt.Printf("%s: %d\n", p.name, p.age)
    }
}
βœ… Output
10 7 20 Alice: 25 Charlie: 30 Bob: 35

πŸ”’ Closures

A closure is a function that "closes over" variables from its enclosing scope. The function retains access to those variables even after the outer function has returned.

How Closures Work

When a function closes over a variable, Go captures a reference to that variable β€” not a copy. This is what makes closures powerful (shared mutable state) and dangerous (concurrent access issues).

go
package main

import "fmt"

// makeCounter returns a closure that maintains its own counter
func makeCounter() func() int {
    count := 0  // captured by the closure
    return func() int {
        count++  // modifies the captured variable
        return count
    }
}

// makeAdder returns a closure that adds a fixed number
func makeAdder(x int) func(int) int {
    return func(y int) int {
        return x + y  // x is captured from makeAdder's scope
    }
}

// Multiple closures can share the same captured variable
func makeCounterPair() (increment func(), decrement func(), get func() int) {
    count := 0  // shared by all three closures
    increment = func() { count++ }
    decrement = func() { count-- }
    get = func() int { return count }
    return
}

// Common closure mistake: capturing loop variable
func badLoopClosures() []func() int {
    funcs := make([]func() int, 3)
    for i := 0; i < 3; i++ {
        funcs[i] = func() int { return i }  // captures i β€” NOT a copy!
    }
    // When called, i is already 3 (loop has finished)
    return funcs
}

// Fix: capture by value using a new variable
func goodLoopClosures() []func() int {
    funcs := make([]func() int, 3)
    for i := 0; i < 3; i++ {
        i := i  // Create a NEW i variable scoped to this iteration
        funcs[i] = func() int { return i }
    }
    return funcs
}

func main() {
    counter1 := makeCounter()
    counter2 := makeCounter()  // independent counter

    fmt.Println(counter1(), counter1(), counter1())  // 1 2 3
    fmt.Println(counter2(), counter2())               // 1 2  (independent!)

    add5 := makeAdder(5)
    add10 := makeAdder(10)
    fmt.Println(add5(3), add10(3))   // 8 13

    inc, dec, get := makeCounterPair()
    inc(); inc(); inc(); dec()
    fmt.Println("count:", get())  // 2

    bad := badLoopClosures()
    fmt.Println("bad closures:", bad[0](), bad[1](), bad[2]())  // 3 3 3 ← bug!

    good := goodLoopClosures()
    fmt.Println("good closures:", good[0](), good[1](), good[2]())  // 0 1 2 βœ“
}
βœ… Output
1 2 3 1 2 8 13 count: 2 bad closures: 3 3 3 good closures: 0 1 2
πŸ”’ Closure Memory Layout
makeCounter() call:
  Stack frame:
    count = 0  ← allocated on HEAP (must outlive makeCounter)

  Returns func:
    closure struct = {
      code: ptr β†’ func() int { count++ }
      env:  ptr β†’ count (the heap-allocated variable)
    }

Each call to counter1() increments the shared heap `count`.
counter1 and counter2 have separate heap `count` variables.

🎭 Higher-Order Functions

Functions that take other functions as arguments or return functions. This enables powerful patterns without generics.

go
package main

import "fmt"

// Map: apply a function to every element
func mapInts(slice []int, fn func(int) int) []int {
    result := make([]int, len(slice))
    for i, v := range slice {
        result[i] = fn(v)
    }
    return result
}

// Filter: keep elements where predicate returns true
func filter(slice []int, pred func(int) bool) []int {
    var result []int
    for _, v := range slice {
        if pred(v) {
            result = append(result, v)
        }
    }
    return result
}

// Reduce: fold a slice into a single value
func reduce(slice []int, initial int, fn func(int, int) int) int {
    acc := initial
    for _, v := range slice {
        acc = fn(acc, v)
    }
    return acc
}

// Compose: return a function that is f(g(x))
func compose(f, g func(int) int) func(int) int {
    return func(x int) int {
        return f(g(x))
    }
}

// Memoize: cache function results
func memoize(fn func(int) int) func(int) int {
    cache := make(map[int]int)
    return func(n int) int {
        if v, ok := cache[n]; ok {
            return v
        }
        result := fn(n)
        cache[n] = result
        return result
    }
}

func main() {
    nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

    // Double all elements
    doubled := mapInts(nums, func(x int) int { return x * 2 })
    fmt.Println("doubled:", doubled)

    // Keep only evens
    evens := filter(nums, func(x int) bool { return x%2 == 0 })
    fmt.Println("evens:", evens)

    // Sum all elements
    sum := reduce(nums, 0, func(acc, x int) int { return acc + x })
    fmt.Println("sum:", sum)  // 55

    // Product of all elements
    product := reduce(nums, 1, func(acc, x int) int { return acc * x })
    fmt.Println("product:", product)  // 3628800

    // Compose: triple then add 1
    triple := func(x int) int { return x * 3 }
    addOne := func(x int) int { return x + 1 }
    tripleAddOne := compose(addOne, triple)  // addOne(triple(x))
    fmt.Println("compose(5):", tripleAddOne(5))  // 16 = (5*3)+1

    // Memoize fibonacci (with mutable state via closure)
    calls := 0
    expensiveFn := func(n int) int {
        calls++
        return n * n
    }
    memoExpensive := memoize(expensiveFn)
    fmt.Println(memoExpensive(10), memoExpensive(10), memoExpensive(20))
    fmt.Println("actual calls:", calls)  // 2 (10 and 20 only)
}
βœ… Output
doubled: [2 4 6 8 10 12 14 16 18 20] evens: [2 4 6 8 10] sum: 55 product: 3628800 compose(5): 16 100 100 400 actual calls: 2

⏳ Defer

defer schedules a function call to execute when the surrounding function returns. It's used for cleanup: closing files, releasing locks, logging function exit, recovering from panics.

Defer Execution Order

Deferred calls execute in LIFO (last-in, first-out) order β€” like a stack.

go
package main

import (
    "fmt"
    "os"
)

func deferOrder() {
    fmt.Println("start")
    defer fmt.Println("first defer")   // executes LAST
    defer fmt.Println("second defer")  // executes second
    defer fmt.Println("third defer")   // executes FIRST
    fmt.Println("end")
}
// Output:
// start
// end
// third defer   ← LIFO: last defer runs first
// second defer
// first defer

// Classic use: close a file
func processFile(path string) error {
    f, err := os.Open(path)
    if err != nil {
        return err
    }
    defer f.Close()  // Close runs when processFile returns, regardless of how

    // Process the file...
    // If we return early due to error, Close still runs
    return nil
}

// Defer with a mutex β€” guaranteed unlock
// import "sync"
// func withLock(mu *sync.Mutex) {
//     mu.Lock()
//     defer mu.Unlock()  // always unlocks, even on panic
//     // critical section
// }

// Defer captures values at defer time for VALUE args
// But references (pointers, closures) capture later state
func deferCapture() {
    x := 10
    defer fmt.Println("deferred x =", x)  // x captured as 10 NOW
    x = 20
    fmt.Println("current x =", x)
    // Output:
    // current x = 20
    // deferred x = 10  ← captured at defer time!
}

// Named return + defer β€” modify return value from defer
func countTo(n int) (result int) {
    defer func() {
        result += 100  // modifies the named return 'result'
    }()
    for i := 1; i <= n; i++ {
        result += i
    }
    return  // bare return, then defer modifies result
}

func main() {
    deferOrder()
    fmt.Println("---")
    deferCapture()
    fmt.Println("---")
    fmt.Println("countTo(5):", countTo(5))  // 1+2+3+4+5+100 = 115
}
βœ… Output
start end third defer second defer first defer --- current x = 20 deferred x = 10 --- countTo(5): 115
🎯 Defer Best Practices
  • Use defer for cleanup: close files, unlock mutexes, close DB connections
  • Avoid defer in tight loops β€” each defer adds overhead (function call + stack entry)
  • Remember LIFO order when multiple defers exist
  • Named returns + defer = the idiomatic Go way to log function exit times or annotate errors

πŸŒ€ Recursion

go
package main

import "fmt"

// Classic: Fibonacci with memoization
func fibMemo(n int, memo map[int]int) int {
    if n <= 1 {
        return n
    }
    if v, ok := memo[n]; ok {
        return v
    }
    result := fibMemo(n-1, memo) + fibMemo(n-2, memo)
    memo[n] = result
    return result
}

// Factorial β€” O(n) time, O(n) stack space
func factorial(n int) int {
    if n <= 1 {
        return 1  // base case β€” stops recursion
    }
    return n * factorial(n-1)  // recursive case
}

// Tail-recursive factorial β€” compiler may optimise this
func factTail(n, acc int) int {
    if n <= 1 {
        return acc
    }
    return factTail(n-1, n*acc)
}

// Binary search β€” recursive
func binarySearch(arr []int, target, low, high int) int {
    if low > high {
        return -1  // not found
    }
    mid := (low + high) / 2
    if arr[mid] == target {
        return mid
    } else if arr[mid] < target {
        return binarySearch(arr, target, mid+1, high)
    }
    return binarySearch(arr, target, low, mid-1)
}

func main() {
    memo := make(map[int]int)
    fmt.Println("fib(10):", fibMemo(10, memo))  // 55
    fmt.Println("fib(40):", fibMemo(40, memo))  // 102334155

    fmt.Println("5! =", factorial(5))         // 120
    fmt.Println("10! =", factTail(10, 1))     // 3628800

    arr := []int{1, 3, 5, 7, 9, 11, 13, 15}
    fmt.Println("search 7:", binarySearch(arr, 7, 0, len(arr)-1))   // 3
    fmt.Println("search 6:", binarySearch(arr, 6, 0, len(arr)-1))   // -1
}
βœ… Output
fib(10): 55 fib(40): 102334155 5! = 120 10! = 3628800 search 7: 3 search 6: -1

πŸ—οΈ Call Stack & Stack Frames

Every function call creates a stack frame on the goroutine's stack. Understanding this helps you reason about performance and recursion depth.

πŸ“Š Stack Frames During Execution
main() calls factorial(3):

  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  ← top of stack (grows down)
  β”‚  factorial(1)   β”‚
  β”‚  n = 1          β”‚
  β”‚  return addr    β”‚
  β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
  β”‚  factorial(2)   β”‚
  β”‚  n = 2          β”‚
  β”‚  return addr    β”‚
  β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
  β”‚  factorial(3)   β”‚
  β”‚  n = 3          β”‚
  β”‚  return addr    β”‚
  β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
  β”‚     main()      β”‚
  β”‚  return addr    β”‚
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  ← bottom of stack

Each return pops the top frame.
Too many frames = stack overflow (goroutine panics).
go
package main

import (
    "fmt"
    "runtime"
)

func printStackTrace() {
    buf := make([]byte, 4096)
    n := runtime.Stack(buf, false)
    fmt.Printf("Stack trace:\n%s\n", buf[:n])
}

func c() { printStackTrace() }
func b() { c() }
func a() { b() }

func main() {
    a()
}
// Output shows the full call chain: main β†’ a β†’ b β†’ c

🎀 Interview Questions

1
What is a closure? How does it capture variables?
β–Ά
Detailed Answer

A closure is a function value that references variables from outside its body. When the outer function returns, the closure retains access to those variables.

Closures capture variables by reference β€” they share the same memory location as the original variable. If the outer variable changes, the closure sees the new value. This is both the power and the danger: multiple closures can share state, but this creates race conditions in concurrent code.

The classic bug: capturing loop variables in goroutines. Fix: create a new variable with the same value inside the loop to force a copy.

Memory impact: Captured variables escape to the heap β€” they must outlive the function that created them. This adds GC pressure, so closures in hot paths can be a performance concern.

2
Explain the execution order of defer. What is defer used for?
β–Ά
Detailed Answer

Deferred function calls execute in LIFO (last-in, first-out) order when the surrounding function returns β€” whether by returning normally, returning an error value, or panicking. This makes defer ideal for cleanup.

Primary uses: (1) closing resources (file handles, DB connections, HTTP responses); (2) unlocking mutexes; (3) annotating elapsed time (defer a timer stop at function start); (4) recovering from panics; (5) modifying named return values.

Arguments to deferred functions are evaluated immediately when defer is called, not when the deferred function executes. This means defer fmt.Println(x) captures x's value at the defer statement, not at return time.

Performance: Each defer statement has overhead (roughly equivalent to a function call). Avoid defer inside tight loops.

3
What are named return values? When should you use them?
β–Ά
Detailed Answer

Named return values pre-declare variables that will be returned. They allow a "bare return" (just return with no arguments), which implicitly returns the named variables.

Best use cases: (1) when defer needs to modify the return value (error annotation, cleanup); (2) when the function is short and the names serve as documentation; (3) when multiple return values would be confusing without names.

Avoid named returns when: the function is long (bare return hides what's being returned); or you mix bare returns and explicit returns (confusing).

πŸ§ͺ Quiz

🧩
Functions
5 questions
1. What is the output of: defer fmt.Println("A"); defer fmt.Println("B"); defer fmt.Println("C")?
2. After for i:=0; i<3; i++ { funcs[i] = func() int { return i } }, calling funcs[0]() returns:
3. After s := []int{1,2,3}; f(s...) where f does nums[0]=99, what is s[0]?
4. x := 5; defer fmt.Println(x); x = 10. What does the deferred call print?
5. A closure captures outer variables by:
β€”

✏️ Exercises

Exercise 1
Pipeline Function
⭐⭐ Intermediate

Write a pipeline function that takes a slice of integers and a variadic list of func([]int) []int transform functions, and applies them in sequence. Test it with: filter evens, double all, sum.

go
package main

import "fmt"

func pipeline(data []int, transforms ...func([]int) []int) []int {
    result := data
    for _, fn := range transforms {
        result = fn(result)
    }
    return result
}

func main() {
    data := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

    filterEvens := func(s []int) []int {
        var r []int
        for _, v := range s {
            if v%2 == 0 { r = append(r, v) }
        }
        return r
    }
    doubleAll := func(s []int) []int {
        r := make([]int, len(s))
        for i, v := range s { r[i] = v * 2 }
        return r
    }

    result := pipeline(data, filterEvens, doubleAll)
    fmt.Println(result)  // [4 8 12 16 20]
}
Exercise 2
Retry with Backoff
⭐⭐⭐ Advanced

Write a retry function that takes a function fn func() error, a max number of attempts, and a backoff duration. It should retry fn up to maxAttempts times, waiting longer between each retry. Return nil on success or the last error.

go
package main

import (
    "fmt"
    "time"
    "errors"
)

func retry(fn func() error, maxAttempts int, baseDelay time.Duration) error {
    var lastErr error
    for attempt := 1; attempt <= maxAttempts; attempt++ {
        if err := fn(); err == nil {
            return nil  // success!
        } else {
            lastErr = err
            if attempt < maxAttempts {
                wait := baseDelay * time.Duration(attempt)
                fmt.Printf("Attempt %d failed: %v. Retrying in %v...\n", attempt, err, wait)
                time.Sleep(wait)
            }
        }
    }
    return fmt.Errorf("all %d attempts failed: %w", maxAttempts, lastErr)
}

func main() {
    attempts := 0
    fn := func() error {
        attempts++
        if attempts < 3 {
            return errors.New("service unavailable")
        }
        return nil  // succeed on 3rd attempt
    }

    err := retry(fn, 5, 100*time.Millisecond)
    if err != nil {
        fmt.Println("Final error:", err)
    } else {
        fmt.Println("Success on attempt:", attempts)
    }
}

πŸ“Œ Cheat Sheet

πŸ“Œ Functions β€” Quick Reference
Basic
func name(a, b int) int { }
Multiple returns
func f() (int, error) { }
Named returns
func f() (n int, err error) { }
Variadic
func f(args ...int) { }
Spread slice
f(slice...)
Anonymous func
fn := func(x int) int { return x }
IIFE
func() { }()
Defer
defer f() // LIFO order
Closure captures
by reference (use i := i to copy)
Function type
var fn func(int) int