Go Gopher How to Go

Recursion is a technique where a function calls itself to solve a problem by breaking it down into smaller subproblems. Go supports recursive functions naturally, making it useful for problems that have a recursive structure.

💡 Key Points

  • Recursion involves a function calling itself to solve smaller instances of a problem
  • Must have a base case to prevent infinite recursion
  • Each recursive call should make progress toward the base case
  • Go doesn't optimize tail recursion - consider iterative alternatives for deep recursion
  • Stack overflow can occur with too many recursive calls
  • Memoization can dramatically improve performance for overlapping subproblems
  • Natural fit for tree/graph traversal, divide-and-conquer algorithms
  • Can be less efficient than iteration due to function call overhead

Classic Recursion: Factorial

The factorial function is a classic example of recursion. It calculates n! = n * (n-1) * ... * 1:

package main

import "fmt"

// Recursive factorial function
func factorial(n int) int {
    // Base case: factorial of 0 or 1 is 1
    if n == 0 || n == 1 {
        return 1
    }
    // Recursive case: n! = n * (n-1)!
    return n * factorial(n-1)
}

func main() {
    fmt.Println("5! =", factorial(5))
    fmt.Println("7! =", factorial(7))
    fmt.Println("10! =", factorial(10))
}
Output:
5! = 120
7! = 5040
10! = 3628800

Fibonacci Sequence

Another classic recursive example is calculating Fibonacci numbers:

package main

import "fmt"

// Recursive Fibonacci function
func fibonacci(n int) int {
    // Base cases
    if n <= 1 {
        return n
    }
    // Recursive case: fib(n) = fib(n-1) + fib(n-2)
    return fibonacci(n-1) + fibonacci(n-2)
}

// Efficient version with memoization
func fibMemo(n int, memo map[int]int) int {
    if n <= 1 {
        return n
    }
    if val, ok := memo[n]; ok {
        return val
    }
    memo[n] = fibMemo(n-1, memo) + fibMemo(n-2, memo)
    return memo[n]
}

func main() {
    fmt.Println("Fibonacci sequence:")
    for i := 0; i < 10; i++ {
        fmt.Printf("fib(%d) = %d\n", i, fibonacci(i))
    }
    
    // Using memoization for efficiency
    memo := make(map[int]int)
    fmt.Println("\nfib(40) with memo:", fibMemo(40, memo))
}
Output:
Fibonacci sequence:
fib(0) = 0
fib(1) = 1
fib(2) = 1
fib(3) = 2
fib(4) = 3
fib(5) = 5
fib(6) = 8
fib(7) = 13
fib(8) = 21
fib(9) = 34

fib(40) with memo: 102334155

Recursion Components

ComponentDescriptionExample
Base CaseCondition that stops recursionif n == 0 { return 1 }
Recursive CaseFunction calls itself with modified argsreturn n * factorial(n-1)
ProgressEach call moves toward base casen decreases: n → n-1 → n-2 → ... → 0
Return ValueResults combine back up the call stack5! = 5 * 4! = 5 * 24 = 120

Recursion Patterns

PatternDescriptionUse Case
Linear RecursionOne recursive call per invocationFactorial, sum of list
Binary RecursionTwo recursive calls per invocationFibonacci, binary tree traversal
Tail RecursionRecursive call is last operationOptimizable by compiler (not in Go)
Mutual RecursionFunctions call each other recursivelyState machines, parsers
MemoizationCache results to avoid recomputationDynamic programming problems

Recursion vs Iteration

AspectRecursionIteration
Code ClarityOften simpler and more elegantCan be more verbose
Memory UsageUses call stack (can overflow)Constant memory for loops
PerformanceFunction call overheadGenerally faster
Problem FitNatural for tree/graph problemsBetter for sequential processing
Tail OptimizationNot optimized in GoN/A

Best Practices

PracticeRecommendationReason
Base CaseAlways define clear base case(s)Prevents infinite recursion and stack overflow
ProgressEnsure each call moves toward base caseGuarantees termination
Depth LimitConsider maximum recursion depthGo's stack size is limited
MemoizationCache results for overlapping subproblemsImproves performance dramatically
Tail RecursionConvert to iteration in GoGo doesn't optimize tail calls
TestingTest with small inputs firstDebug recursion logic before scaling