05 — Functions
Declaration Forms and Return Patterns
go
// ┌─────────────────────────────────────────────────────────────────────┐
// │ Form │ Use Case │
// │ ─────────────────────── │ ──────────────────────────────────────── │
// │ func f(a, b int) int │ simple function │
// │ func f(a, b int)(int,error) │ Go's (value, error) convention │
// │ func f()(q, r int) │ named returns (defer modification) │
// │ func f(a int, opts ...Opt) T │ variadic (builder/options pattern) │
// │ func f() func() int │ returning a closure (stateful function) │
// │ var f func(int)int = ... │ function as a value │
// └─────────────────────────────────────────────────────────────────────┘
// Multiple returns — Go's primary error-handling mechanism:
func fetchUser(id int64) (*User, error) {
if id <= 0 {
return nil, fmt.Errorf("fetchUser: invalid id %d", id)
}
return &User{ID: id}, nil
}
// Named returns — pre-declared, zero-initialized, can be modified by defer:
func divide(a, b int) (result int, err error) {
if b == 0 {
err = errors.New("divide by zero")
return // naked return — returns (result=0, err=error)
}
result = a / b
return // returns (result=quotient, err=nil)
}
Named returns — the real use case
go
// ❌ ANTI-PATTERN: naked returns in long functions (unreadable)
func process(data []byte) (result []byte, err error) {
// ... 50 lines of code ...
result = transform(data)
// ... 30 more lines ...
return // ← what does this return? reader must scan entire function
}
// ✅ CORRECT: named returns for defer-based error decoration and timing:
func timedOperation(ctx context.Context) (result int, err error) {
start := time.Now()
defer func() {
// Log the duration and decorate the error on the way out:
elapsed := time.Since(start)
if err != nil {
err = fmt.Errorf("timedOperation (took %v): %w", elapsed, err)
}
log.Printf("timedOperation took %v", elapsed)
}()
// ... actual work ...
result = 42
return result, nil
}
// ✅ Panic recovery via deferred named return:
func safeExec(fn func() error) (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("recovered from panic: %v", r)
}
}()
return fn()
}
Variadic Functions — The Options Pattern
go
// Variadic: `...T` becomes a `[]T` inside the function. Must be last param.
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
// sum(1, 2, 3) → 6
// nums := []int{1, 2, 3}; sum(nums...) → 6 (spread a slice)
// ─── Production pattern: functional options ───
type Server struct {
addr string
port int
tls bool
timeout time.Duration
}
type Option func(*Server) // function type that mutates the server config
func WithPort(p int) Option {
return func(s *Server) { s.port = p }
}
func WithTLS(cfg *tls.Config) Option {
return func(s *Server) { s.tls = true }
}
func WithTimeout(d time.Duration) Option {
return func(s *Server) { s.timeout = d }
}
func NewServer(addr string, opts ...Option) *Server {
s := &Server{
addr: addr,
port: 8080, // sensible default
timeout: 30 * time.Second,
}
for _, opt := range opts {
opt(s) // apply each option
}
return s
}
// Usage — readable, extensible, zero config structs:
srv := NewServer(":8080",
WithPort(9090),
WithTimeout(10*time.Second),
WithTLS(tlsConfig),
)
Closures — Capture Semantics
go
// Closures capture variables BY REFERENCE (not by value).
// The captured variable outlives the function that declared it —
// it's moved to the heap (escape analysis detects this).
func counter() func() int {
n := 0 // captured by the closure below
return func() int {
n++ // modifies the SAME n across calls
return n
}
}
// c := counter(); c() → 1; c() → 2; c() → 3
// Each call to counter() creates a NEW n (independent counters).
// ─── Generator pattern ───
func fibonacci() func() int {
a, b := 0, 1
return func() int {
a, b = b, a+b
return a
}
}
// f := fibonacci(); f() → 1; f() → 1; f() → 2; f() → 3; f() → 5
// ─── The loop variable capture trap (pre-Go 1.22) ───
func captureTrap() {
var fns []func()
for i := 0; i < 3; i++ {
fns = append(fns, func() { fmt.Println(i) }) // captures i by reference
}
for _, f := range fns {
f()
}
// Go 1.21: 3 3 3 (all see the final i=3 — single variable reused)
// Go 1.22+: 0 1 2 (each iteration has its own i — spec change)
}
// ✅ Pre-1.22 fix (still safe on 1.22+):
func captureFixed() {
var fns []func()
for i := 0; i < 3; i++ {
i := i // shadow — creates a new i per iteration
fns = append(fns, func() { fmt.Println(i) })
}
// 0 1 2 on all versions
}
Functions as Values and Types
go
// Function types are first-class — assignable, passable, returnable.
// A function type is spelled: func(paramTypes) returnTypes
type Mapper[T, U any] func(T) U // generic function type (Go 1.18+)
// Higher-order: function that takes a function:
func mapSlice[T, U any](items []T, fn Mapper[T, U]) []U {
result := make([]U, len(items))
for i, item := range items {
result[i] = fn(item)
}
return result
}
// Usage:
doubled := mapSlice([]int{1, 2, 3}, func(x int) int { return x * 2 })
// [2 4 6]
// Function type as a field — strategy pattern:
type Processor struct {
transform func([]byte) []byte // injected strategy
}
func (p *Processor) Process(data []byte) []byte {
return p.transform(data)
}
// ⚠️ Function types are distinct — no implicit conversion:
// func(int) int ≠ func(int64) int (different parameter types)
// func() ≠ func() error (different return types)
defer — The Execution Model
go
// defer schedules a function call to run when the enclosing function returns.
// Key properties:
// 1. LIFO order (last deferred runs first)
// 2. Arguments evaluated IMMEDIATELY (at defer time, not run time)
// 3. Runs on panic (before the program crashes) — but NOT on os.Exit
// 4. Has a small overhead (~35ns per defer pre-1.14, ~1ns open-coded 1.14+)
func deferOrder() {
// LIFO unwind:
defer fmt.Println("1") // runs 4th (last)
defer fmt.Println("2") // runs 3rd
defer fmt.Println("3") // runs 2nd
fmt.Println("4") // runs 1st (immediate)
// Output: 4, 3, 2, 1
}
// ─── Argument evaluation timing ───
func deferArgEval() {
i := 1
defer fmt.Println(i) // prints 1 — i evaluated NOW (at defer time)
i = 2
defer func() { fmt.Println(i) }() // prints 2 — i evaluated at RUN time
// Output: 2, 1 (LIFO: the closure runs first, then the println(i))
}
// ─── Resource cleanup (the primary use) ───
func readFile(path string) ([]byte, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close() // guaranteed to run on return, early return, OR panic
// This is why defer exists — you can't forget to close.
data, err := io.ReadAll(f)
if err != nil {
return nil, err // f.Close() runs here
}
return data, nil // f.Close() runs here
}
// ─── defer in reverse order (nested resources) ───
func nestedResources() error {
db, _ := sql.Open("postgres", dsn)
defer db.Close() // runs LAST (opened first, closed last)
conn, _ := db.Conn(context.Background())
defer conn.Close() // runs FIRST (opened last, closed first)
rows, _ := conn.QueryContext(context.Background(), "SELECT 1")
defer rows.Close() // runs before conn.Close
// LIFO ensures rows → conn → db (inner to outer)
return nil
}
defer in loops — the resource leak
go
// ❌ ANTI-PATTERN: defer in a loop — resources accumulate until function returns
func processFilesBad(paths []string) error {
for _, p := range paths {
f, err := os.Open(p)
if err != nil {
return err
}
defer f.Close() // ALL files stay open until processFilesBad returns!
// With 10000 files → "too many open files" (EMFILE)
if err := process(f); err != nil {
return err
}
}
return nil
}
// ✅ CORRECT: extract loop body into a function — defer runs per iteration
func processFilesGood(paths []string) error {
for _, p := range paths {
if err := processOneFile(p); err != nil {
return err
}
}
return nil
}
func processOneFile(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close() // runs when processOneFile returns — file closed per iteration
return process(f)
}
defer performance — open-coded defer
go
// Go 1.14+ "open-coded defer" optimization:
// - If a function has ≤8 defers AND none are in loops
// - The compiler inlines the defer logic (no runtime deferproc call)
// - Cost drops from ~35ns to ~1-2ns per defer
//
// This means defer is now effectively free for the common case (a few
// resource cleanups in a normal function). Don't avoid defer for perf
// unless profiling shows it's a bottleneck (extremely rare).
//
// defer is still expensive when:
// - In a loop (accumulates, not open-coded)
// - In a function with >8 defers
// - The deferred function is dynamic (defer f where f is a variable)
Anonymous Functions and IIFEs
go
// Immediately-invoked function expression (IIFE):
result := func(x int) int {
return x * 2
}(5) // 10
// Use case: scoped computation without polluting the outer scope:
func handler(w http.ResponseWriter, r *http.Request) {
// Parse and validate in an IIFE — keeps temp vars local:
input, err := func() (string, error) {
body, err := io.ReadAll(r.Body)
if err != nil {
return "", err
}
return strings.TrimSpace(string(body)), nil
}()
if err != nil {
http.Error(w, err.Error(), 400)
return
}
_ = input
}
// Goroutine launch — the most common IIFE:
go func() {
defer wg.Done()
// concurrent work
}()
💡 Tips & Tricks
- Idiom: use the functional options pattern (
WithPort(8080),WithTimeout(...)) for constructors with many optional parameters — it's more readable than a config struct with many nilable fields, and it's extensible (new options don't break existing callers). - Idiom: use named returns +
deferfor error decoration, timing, and panic recovery — this is the legitimate use of named returns. Don't use naked returns in long functions for readability. - Performance:
deferis nearly free in Go 1.14+ for the open-coded case (≤8 defers, no loops). Don't avoiddefer f.Close()for performance reasons — the safety is worth the ~1ns. - Idiom: pair every resource acquisition (
os.Open,sql.Open,os.Create,lock.Lock) with adeferof the corresponding release — this is the #1 defer use case. Defers run in LIFO order, matching nested resource lifetimes. - Safety:
deferruns on panic but NOT onos.Exit— never callos.Exitinside a function with cleanup defers. Return an error tomainand callos.Exitthere. - Debug:
deferarguments are evaluated at defer time —defer fmt.Println(i)capturesi's current value. To capture the value at return time, use a closure:defer func() { fmt.Println(i) }().
⚠️ Edge Cases & Gotchas
deferin a loop accumulates: deferred calls don't run until the function returns — all resources stay open. Extract the loop body into a function.deferdoesn't run onos.Exit:os.Exit(n)terminates immediately, skipping all defers. Return errors tomainand exit there.deferargument evaluation timing:defer f(i)capturesiat defer time;defer func() { f(i) }()capturesiat run time. The difference matters when the variable changes between defer and return.- Naked returns in long functions:
returnwith no values + named returns is opaque in 50+ line functions. Use explicit returns for readability. - Closure capture by reference: closures capture variables by reference, not by value. A loop variable captured by a closure sees the final value (pre-1.22). Pass as argument or shadow (
i := i). ...spread requires a slice:sum(nums...)works withnums=[]int;sum(5...)is a compile error. The spread is for variadic calls only.- Function types are distinct:
func(int) intandfunc(int64) intare different types — no implicit conversion. This matches Go's no-implicit-conversion rule. - Multiple returns must be fully received:
f, err := os.Open(...)— both values must be used or explicitly discarded (_).f := os.Open(...)is a compile error. - Variadic
nilspread:sum(nil...)where the slice is[]int(nil)— works (zero iterations). Butvar s []int; sum(s...)with a nil slice also works (range over nil slice = zero iterations). deferandrecover:recover()only works inside a deferred function. Callingrecover()outside defer returns nil even during a panic. This is the only way to catch a panic.
🧠 Quick Quiz
go
func f() (result int) {
defer func() { result *= 2 }()
defer func() { result += 10 }()
return 5
}
What does f() return?
Answer
f() returns 20.
Execution order:
return 5setsresult = 5- Defers run in LIFO order:
- First defer (registered second):
result += 10→result = 15 - Second defer (registered first):
result *= 2→result = 30
- First defer (registered second):
Wait — that gives 30. Let me re-check.
Actually:
return 5→result = 5, then defers run:- LIFO: the LAST registered defer runs first:
defer func() { result += 10 }()was registered second, runs first →result = 15defer func() { result *= 2 }()was registered first, runs second →result = 30
f() returns 30.
The key insight: return 5 doesn't immediately return — it assigns 5 to the named return result, then deferred functions run (in LIFO order), and THEN the function returns with the modified result.
📚 What's Next
→ 06 — Control Flow — if/for/switch/select, Go 1.22 loop scoping, labeled breaks, and the absence of while.