Skip to content

Go Recursion

Recursion is a problem-solving technique where a function calls itself, directly or indirectly, to solve a smaller instance of the same problem. A recursive function must have one or more base cases that terminate the recursion, and a recursive step that reduces the problem size towards a base case.

The general structure of a recursive function:

func recursiveFunction(input parameters) output_type {
if isBaseCase(input parameters) {
// Solve directly for the base case
return base_case_solution
} else {
// Recursive step: break down the problem
modified_input := modify(input parameters) // Move towards a base case
partial_solution := recursiveFunction(modified_input)
// Combine partial_solution to form the solution for the current input
return combine(partial_solution, current_input_data)
}
}

Go fully supports recursive functions. However, it’s crucial to design recursive functions carefully to ensure they always reach a base case, otherwise, they will recurse infinitely, leading to a stack overflow error (as each function call consumes stack memory).

The factorial of a non-negative integer n (denoted n!) is the product of all positive integers up to n. By definition, 0! = 1.

package main
import "fmt"
// factorial calculates n!
// This function assumes n >= 0. For production code, error handling for n < 0 might be needed.
func factorial(n uint64) uint64 {
// Base case: if n is 0 or 1, factorial is 1.
if n <= 1 {
return 1
}
// Recursive step: n * factorial(n-1)
return n * factorial(n-1)
}
func main() {
var num uint64
num = 5
fmt.Printf("Factorial of %d is %d\n", num, factorial(num)) // Output: Factorial of 5 is 120
num = 0
fmt.Printf("Factorial of %d is %d\n", num, factorial(num)) // Output: Factorial of 0 is 1
// For larger numbers, ensure the type can hold the result.
// uint64 can hold up to 20! (factorial(20)). factorial(21) will overflow uint64.
num = 20
fmt.Printf("Factorial of %d is %d\n", num, factorial(num))
// Output: Factorial of 20 is 2432902008176640000
}

The Fibonacci sequence is a series where each number is the sum of the two preceding ones, usually starting with 0 and 1: F(0) = 0, F(1) = 1, F(n) = F(n-1) + F(n-2) for n > 1.

package main
import "fmt"
// fibonacci returns the n-th Fibonacci number.
// This naive recursive implementation is inefficient for larger n due to repeated computations.
func fibonacci(n int) int {
if n <= 0 { // Base case for F(0)
return 0
}
if n == 1 { // Base case for F(1)
return 1
}
// Recursive step
return fibonacci(n-1) + fibonacci(n-2)
}
func main() {
fmt.Println("First 10 Fibonacci numbers:")
for i := 0; i < 10; i++ {
fmt.Printf("%d ", fibonacci(i))
}
fmt.Println() // Output: 0 1 1 2 3 5 8 13 21 34
}
  • Elegance vs. Efficiency: Recursion can lead to very elegant and readable code for problems that are naturally recursive (e.g., tree traversals, quicksort, mergesort). However, it often incurs more overhead than iterative solutions due to function call costs.
  • Stack Overflow: Deep recursion can exhaust the call stack memory, leading to a stack overflow error. Go’s goroutine stacks start small and can grow, but there’s still a limit.
  • Repeated Computations: Some recursive algorithms, like the naive Fibonacci example, can be very inefficient because they recompute the same subproblems multiple times. Techniques like memoization (caching results of subproblems) or converting to an iterative dynamic programming approach can significantly improve performance.
  • Tail Call Optimization (TCO): Some languages optimize certain types of recursion called tail recursion into iteration, avoiding stack growth. Go’s compiler currently does not guarantee TCO, so deep tail-recursive functions can still lead to stack overflows.

While Go supports recursion effectively, it’s often wise to consider iterative alternatives for performance-critical sections or when dealing with potentially very deep recursion, unless the recursive solution is significantly clearer and the depth is known to be limited.