Lab 02: Functions, Closures & Defer

Objective

Master Go functions: multiple return values, named returns, variadic functions, first-class functions, closures, and defer.

Time

25 minutes

Prerequisites

  • Lab 01 (Hello World & Go Basics)

Tools

  • Docker image: zchencow/innozverse-go:latest


Lab Instructions

Step 1: Multiple Return Values

docker run --rm zchencow/innozverse-go:latest go run - << 'EOF'
package main

import (
    "errors"
    "fmt"
    "strconv"
)

// Multiple returns — the Go way to handle errors
func safeDivide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

// Named return values — self-documenting
func stats(nums []float64) (mean, stddev float64, err error) {
    if len(nums) == 0 {
        err = errors.New("empty slice")
        return
    }
    for _, n := range nums {
        mean += n
    }
    mean /= float64(len(nums))
    for _, n := range nums {
        diff := n - mean
        stddev += diff * diff
    }
    stddev = stddev / float64(len(nums))
    return // naked return — returns named values
}

func parseInts(strs []string) ([]int, []error) {
    results := make([]int, 0, len(strs))
    errs    := make([]error, 0)
    for _, s := range strs {
        n, err := strconv.Atoi(s)
        if err != nil {
            errs = append(errs, fmt.Errorf("cannot parse %q: %w", s, err))
        } else {
            results = append(results, n)
        }
    }
    return results, errs
}

func main() {
    if result, err := safeDivide(10, 3); err != nil {
        fmt.Println("Error:", err)
    } else {
        fmt.Printf("10/3 = %.4f\n", result)
    }

    _, err := safeDivide(1, 0)
    fmt.Println("Error:", err)

    data := []float64{2, 4, 4, 4, 5, 5, 7, 9}
    mean, variance, err := stats(data)
    fmt.Printf("mean=%.2f variance=%.2f err=%v\n", mean, variance, err)

    nums, errs := parseInts([]string{"1", "2", "abc", "4", "xyz"})
    fmt.Println("Parsed:", nums)
    fmt.Println("Errors:", len(errs))
}
EOF

💡 Named return values serve as documentation AND allow naked returns. However, use them sparingly — in long functions, naked returns reduce clarity. The main use case is deferring a return value modification (e.g., in the error path).

📸 Verified Output:


Step 2: Variadic & Functional Arguments

💡 The functional options pattern (WithXxx functions returning func(*Config)) is idiomatic Go for configuring structs with many optional parameters. It's used by grpc.Dial, http.Server, and most major Go libraries. It avoids long constructors and is backward compatible when new options are added.

📸 Verified Output:


Step 3: First-Class Functions & Higher-Order Functions

💡 Go 1.18+ generics let you write filter[T any], mapSlice[T, U any] that work with any type. Before generics, Go developers wrote separate versions for each type. The [T any] syntax means "T can be any type." [T comparable] would require T to support ==.

📸 Verified Output:


Step 4: Closures

💡 Closures capture variables by reference, not by value. Each call to counter() creates a new n variable, so c1 and c2 have independent state. This is the key difference from just passing values — closures maintain state between calls without using global variables or structs.

📸 Verified Output:


Step 5: Defer

💡 defer runs when the function returns, in LIFO order. It's idiomatic Go for cleanup: defer file.Close(), defer mu.Unlock(), defer db.Close(). Combined with recover(), defer implements the only exception-like mechanism in Go. Unlike try/finally, defer is attached to the function, not a block.

📸 Verified Output:


Step 6: Recursion & Tail Calls

💡 Go does not optimize tail calls — deep recursion will cause a stack overflow. For production code, prefer iterative solutions or explicit stacks. Go's goroutine stacks start small (2KB) and grow automatically, so moderate recursion (hundreds of levels) is fine.

📸 Verified Output:


Step 7: init() & Package-Level Functions

💡 init() functions run automatically before main(), after package-level variables are initialized. A package can have multiple init() functions — they run in source order. Use init() for one-time setup like loading config, registering drivers, or pre-computing lookup tables.

📸 Verified Output:


Step 8: Capstone — Pipeline of Functions

📸 Verified Output:


Summary

Concept
Key points

Multiple returns

func f() (T, error) — idiomatic Go error handling

Variadic

func f(args ...T) — pass slice with slice...

Functional options

func WithX(val) func(*Config) — flexible configuration

Closures

Capture variables by reference — maintain state

Defer

Runs at function exit, LIFO — use for cleanup

init()

Package setup before main()

Generics

[T any] for type-safe reusable functions

Further Reading

Last updated