Go 函数详解 — 多返回值、闭包与 defer

函数是组织代码的基本单位。Go 的函数吸收了 C 家族的简洁,又加入了几项现代语言的招牌特性:多返回值命名返回值一等公民函数defer 延迟执行。掌握它们,你就能写出地道、健壮的 Go 代码。

一、函数的定义与参数简写

Go 用 func 关键字定义函数,基本形式是 func 名字(参数) 返回类型,后面跟一对花括号 { } 包起来的函数体。和很多语言不同,Go 把类型写在变量名后面——变量在前、类型在后,这是贯穿 Go 全局的约定,习惯了反而觉得清晰。

package main

import "fmt"

// 基本定义:func 函数名(参数列表) 返回类型
func add(a int, b int) int {
    return a + b
}

// 参数简写:连续多个同类型参数可合并
func sub(a, b int) int {
    return a - b
}

// 多返回值:Go 的招牌特性,常用 (结果, 错误)
func div(a, b float64) (float64, error) {
    if b == 0 {
        return 0, fmt.Errorf("除数不能为零")
    }
    return a / b, nil
}

func main() {
    fmt.Println(add(3, 5), sub(10, 4))   // 8 6
    r, err := div(10, 0)
    if err != nil {
        fmt.Println("出错了:", err)
    } else {
        fmt.Println("结果:", r)
    }
}

当多个连续参数类型相同时可以合并:func f(a, b, c int) 等价于 func f(a int, b int, c int)。需要特别提醒——Go 不支持参数默认值,这一点和 Python、JS 不同,通常用可变参数或"函数选项模式"(functional options)来弥补。

二、多返回值与命名返回值

多返回值是 Go 最让人眼前一亮的特性。一个函数可以同时返回"结果"和"错误",调用方必须同时接收,这就把"面对异常"这件事从可选变成必选,极大提升了程序的健壮性。Go 社区的核心约定是:不要用抛异常掩盖错误,而要把错误当成普通返回值,一步一步检查。

命名返回值在返回类型前加括号、给返回值起名字,如 (total int, count int)。好处有两个:一是充当"自带文档",让人一眼看出返回值含义;二是函数末尾可以写"裸 return",自动返回这些命名变量。短函数用裸 return 很简洁,长函数里则建议显式写出返回值,避免读者来回翻找哪个变量被返回。

package main

import "fmt"

// 命名返回值:给返回值起名,可当文档;函数末尾可"裸 return"
func stats(nums ...int) (total int, count int) {
    count = len(nums)
    for _, n := range nums {
        total += n
    }
    return        // 裸 return,自动返回当前的 total 和 count
}

func main() {
    // 注意:nums 是可变参数,在函数内是 []int 切片
    total, n := stats(1, 2, 3, 4, 5)
    fmt.Printf("总和=%d, 个数=%d\n", total, n)
    // 输出:总和=15, 个数=5
}

三、可变参数

参数名前加 ... 表示可变参数,函数内收到的实际上是一个切片。比如 func f(nums ...int) 里的 nums 类型是 []int。调用时可以传任意数量的参数,也可以把一个已有的切片用 s... 的方式"展开"传入。上面的 stats 函数同时演示了可变参数和命名返回值,仔细看一遍就能融会贯通。

四、匿名函数与闭包

Go 的函数是一等公民:可以赋值给变量、当作参数传递、当作返回值。没有名字的函数叫匿名函数,语法是 func(参数) 返回类型 { 函数体 },定义后立即调用或赋给变量都行。下面这段代码把匿名函数和闭包一次讲透。

package main

import "fmt"

func main() {
    // 匿名函数:没有名字,赋值给变量后调用
    add := func(a, b int) int {
        return a + b
    }
    fmt.Println(add(2, 3))   // 5

    // 立即执行的匿名函数(IIFE)
    sq := func(x int) int { return x * x }(6)
    fmt.Println(sq)          // 36

    // 闭包:返回的函数"记住"了外层的 count
    c := makeCounter()
    fmt.Println(c(), c(), c())   // 1 2 3
}

// makeCounter 返回一个会累加的闭包
func makeCounter() func() int {
    count := 0                 // 被闭包捕获,逃逸到堆上
    return func() int {
        count++
        return count
    }
}

闭包(closure)是匿名函数的进阶用法:函数内部引用了外部作用域的变量,这个变量就被"捕获",生命周期跟随闭包一起延长(术语叫"逃逸到堆上")。上面的 makeCounter 每次调用都创建一个独立的 count,返回的函数反复调用会一直累加,而且每次调用 makeCounter 得到的计数器互不干扰。这正是封装私有状态的优雅方式,根本不需要写一个类。

五、defer 延迟调用

defer 后面跟一个函数调用,这个调用会被推迟到外层函数即将返回时才执行。多个 defer 按后进先出(LIFO)的顺序执行——先 defer 的反而后执行,像栈一样。它最常见的用途是资源释放:打开文件后立刻 defer file.Close(),加锁后立刻 defer mutex.Unlock(),保证无论后面走哪个分支、是否中途出错,资源都能被正确释放。

package main

import "fmt"

func main() {
    // defer 延迟到函数返回时执行,多个 defer 后进先出(LIFO)
    defer fmt.Println("第一个 defer,最后执行")
    defer fmt.Println("第二个 defer,先执行")
    fmt.Println("正常代码")

    // 典型用途:成对的"获取/释放",保证资源被回收
    // f, _ := os.Open("a.txt")
    // defer f.Close()        // 函数返回时一定关闭
}
// 输出顺序:
// 正常代码
// 第二个 defer,先执行
// 第一个 defer,最后执行

新手常踩一个坑:defer 语句的参数是在 defer 那一行被执行时就求值的,不是真正调用时。所以 defer fmt.Println(i) 锁定的是当时的 i。另外,在循环里使用 defer 要格外小心——所有 defer 都要等到函数返回才一并执行,循环里大量 defer 可能积压未释放的资源。遇到这种情况,通常把循环体抽成一个独立函数,让资源在每轮结束时立刻释放。

六、递归

Go 完全支持递归——函数直接或间接调用自己。最经典的例子是阶乘和斐波那契数列。写递归有两个要点:必须有一个终止条件(否则会无限递归直到栈溢出),并且每递归一次问题规模要缩小。下面的代码同时给出阶乘和斐波那契,你可以直接拿去运行。

package main

import "fmt"

// 阶乘:n! = n * (n-1)!
func fact(n int) int {
    if n <= 1 {
        return 1              // 终止条件,防止无限递归
    }
    return n * fact(n-1)
}

// 斐波那契:F(n) = F(n-1) + F(n-2)
func fib(n int) int {
    if n < 2 {
        return n
    }
    return fib(n-1) + fib(n-2)
}

func main() {
    fmt.Println(fact(5))   // 120
    fmt.Println(fib(10))   // 55
}

小提示:朴素的斐波那契递归时间复杂度是 O(2^n),n 稍大就会非常慢,实际工程中一般改成循环或用"记忆化"优化。这里主要是为了演示递归写法。

小结

下一篇我们学习 Go 数组与切片,看看 Go 如何处理"一组数据"。上一篇 循环与流程控制

✈️💬