跳转至

Go 语言递归函数

递归函数会直接或间接调用自身。每次调用都有独立栈帧,因此必须设计可到达的终止条件, 并确保每一步都让问题规模向终止条件靠近。

基本递归

package main

import "fmt"

func factorial(n uint64) uint64 {
    if n < 2 {
        return 1
    }
    return n * factorial(n-1)
}

func main() {
    fmt.Println(factorial(5)) // 120
}

n < 2 是终止条件,n-1 是递归推进。示例只说明语法:uint64 的阶乘很快溢出, 生产代码应限制输入、返回错误,或使用 math/big.Int。

递归与迭代的差异

func factorialLoop(n uint64) uint64 {
    result := uint64(1)
    for i := uint64(2); i <= n; i++ {
        result *= i
    }
    return result
}

第一个差异示例:递归版本更直接表达数学定义,迭代版本不为每一步创建调用栈,通常更节省内存。 Go 不保证尾调用优化,把递归调用写在 return 位置也不能假设栈空间恒定。

树形数据更适合递归

type Node struct {
    Value    int
    Children []*Node
}

func sum(root *Node) int {
    if root == nil {
        return 0
    }
    total := root.Value
    for _, child := range root.Children {
        total += sum(child)
    }
    return total
}

第二个差异示例:树遍历用递归能自然对应节点层级;若树可能极深或输入不可信,应改用显式栈, 以便控制内存、深度和取消逻辑。

记忆化

朴素 Fibonacci 会重复计算相同子问题,时间复杂度呈指数增长。可把结果放入 map:

func fib(n int, memo map[int]uint64) uint64 {
    if n < 2 {
        return uint64(n)
    }
    if value, ok := memo[n]; ok {
        return value
    }
    memo[n] = fib(n-1, memo) + fib(n-2, memo)
    return memo[n]
}

共享 memo 给多个 goroutine 时需要同步;单次计算中也要限制 n,避免整数溢出和过深递归。

常见错误

  • 缺少终止条件,最终耗尽栈空间。
  • 递归参数未向终止条件推进,例如负数输入不断减小。
  • 使用指数级重复递归,却没有记忆化或动态规划。
  • 假设 Go 会进行尾递归优化。
  • 遍历图时不记录已访问节点,遇到环后无限递归。

性能与工程实践

Go 的 goroutine 栈会按需增长,但递归深度仍不是无限的,增长和函数调用也有成本。解析用户可控的 嵌套数据时应设置最大深度,防止资源耗尽。对链表、深目录或图遍历,显式 []T 栈通常更安全; 对深度受控的语法树和树形配置,递归通常更清晰。优化前用基准测试和 CPU profile 找到真实热点。