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.
π― Learning Objectives
- 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
defercorrectly 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).
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
}π·οΈ 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.
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)
}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.
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
}π€ 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.
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)
}
}π 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).
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 β
}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.
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)
}β³ 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.
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
}- 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
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
}ποΈ 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.
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).
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
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.
defer. What is defer used for?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.
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
defer fmt.Println("A"); defer fmt.Println("B"); defer fmt.Println("C")?for i:=0; i<3; i++ { funcs[i] = func() int { return i } }, calling funcs[0]() returns:s := []int{1,2,3}; f(s...) where f does nums[0]=99, what is s[0]?x := 5; defer fmt.Println(x); x = 10. What does the deferred call print?βοΈ Exercises
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.
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]
}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.
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
func name(a, b int) int { }func f() (int, error) { }func f() (n int, err error) { }func f(args ...int) { }f(slice...)fn := func(x int) int { return x }func() { }()defer f() // LIFO orderby reference (use i := i to copy)var fn func(int) int