Skip to content

第04章 函数 —— 程序的"乐高积木"

本章目标:掌握 Go 函数的多返回值、可变参数、命名返回、闭包、defer,以及函数作为一等公民的玩法。


1. 为什么要写函数?

🌰 生活化比喻:写程序就像做料理。如果每次做饭都从切菜开始,效率太低。我们应该把"切菜"封装成一个函数,需要时调用。

函数 = 把一段做某件事的代码起个名字,下次直接喊它干活。

📊 配套图解:diagrams/function-anatomy.svg


2. 函数声明

2.1 基本语法

go
func add(a int, b int) int {
    return a + b
}

结构func 函数名(参数列表) 返回值类型 { 函数体 }

2.2 简写:相同类型的参数

go
func add(a, b int) int {       // a 和 b 都是 int
    return a + b
}

2.3 多返回值:Go 的招牌特性

go
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("除数不能为 0")
    }
    return a / b, nil
}

result, err := divide(10, 0)

🌰 生活化比喻:相当于一个快递员同时给你送两个包裹。Go 鼓励用第二个返回值返回错误,这就是 error first 哲学。

2.4 命名返回值

go
func split(sum int) (x, y int) {
    x = sum * 4 / 9
    y = sum - x
    return   // 自动返回 x, y(裸返回)
}

⚠️ 慎用裸返回——函数太长时可读性差。

📊 配套图解:diagrams/return-types.svg


3. 可变参数

go
func sum(nums ...int) int {
    total := 0
    for _, n := range nums {
        total += n
    }
    return total
}

sum(1, 2, 3)              // 6
sum(1, 2, 3, 4, 5)        // 15
sum([]int{10, 20, 30}...) // 60(注意 ... 展开)

...int 在函数内会变成 []int。这是 fmt.Println 能接收任意多个参数的奥秘。


4. 函数是一等公民

Go 的函数可以:

  • 赋值给变量
  • 作为参数传递
  • 作为返回值
go
// 赋值
add := func(a, b int) int { return a + b }
fmt.Println(add(2, 3))

// 作为参数
nums := []int{1, 2, 3, 4}
sum := reduce(nums, func(acc, n int) int { return acc + n }, 0)

// 返回函数
func multiplier(factor int) func(int) int {
    return func(n int) int { return n * factor }
}
double := multiplier(2)
fmt.Println(double(5))   // 10

📊 配套图解:diagrams/first-class-function.svg


5. 闭包:函数能"记住"外部变量

go
func counter() func() int {
    count := 0
    return func() int {
        count++
        return count
    }
}

c := counter()
fmt.Println(c())  // 1
fmt.Println(c())  // 2
fmt.Println(c())  // 3

🌰 生活化比喻:闭包像带秘书的总裁——总裁(返回的函数)出门办事,秘书(被引用的变量)跟着他记笔记,外人看不到笔记本。

📊 配套图解:diagrams/closure.svg


6. defer:Go 的"善后特工"

defer 把语句推迟到当前函数返回前执行

go
func readFile(name string) {
    f, _ := os.Open(name)
    defer f.Close()   // 函数退出前自动关闭文件

    // ... 读文件 ...
}

6.1 多个 defer:后进先出(LIFO)

go
defer fmt.Println("1")
defer fmt.Println("2")
defer fmt.Println("3")
// 输出顺序:3 2 1(像栈)

🌰 生活化比喻:你给三个人交代"我走之前你们各做一件事",但是按交代的反序执行,就像把要做的事写在便签纸上叠成一摞——最后贴的最先撕。

6.2 defer 的"取值时机"

go
func tricky() {
    x := 10
    defer fmt.Println("defer:", x)   // 这里 x 是 10 (实参立即求值)
    x = 20
    fmt.Println("now:", x)           // 20
}
// 输出:
// now: 20
// defer: 10

⚠️ defer 的参数在 defer 那一行就已被求值,但函数体到结尾才执行。

闭包形式不一样:

go
func tricky2() {
    x := 10
    defer func() { fmt.Println("defer:", x) }()   // 闭包,x 是引用
    x = 20
}
// 输出:defer: 20

📊 配套图解:diagrams/defer-stack.svg

6.3 defer 的常见用途

场景写法
关闭文件defer f.Close()
释放锁defer mu.Unlock()
关闭 channeldefer close(ch)
关闭网络连接defer conn.Close()
recover panicdefer func(){ recover() }()
性能计时defer trace("xxx")()

7. 递归函数

go
func factorial(n int) int {
    if n <= 1 { return 1 }
    return n * factorial(n-1)
}

⚠️ Go 没有"尾递归优化",深度太大会栈溢出。深度 > 1 万要改循环。


8. 案例:函数工厂 + 中间件

go
// 一个简单的中间件思路:函数包装函数
type Handler func(string) string

func logMiddleware(next Handler) Handler {
    return func(req string) string {
        fmt.Println("[LOG] in:", req)
        resp := next(req)
        fmt.Println("[LOG] out:", resp)
        return resp
    }
}

func handler(req string) string {
    return "Hello, " + req
}

wrapped := logMiddleware(handler)
wrapped("Tom")
// [LOG] in: Tom
// [LOG] out: Hello, Tom

这就是 Web 框架的中间件雏形。


9. 本章小结

✅ Go 函数支持多返回值,常见 (result, error) 模式
✅ 同类型参数可以共享:func f(a, b int)
✅ 可变参数 ...int 在函数内是 []int
✅ 函数是一等公民:可赋值、可传参、可返回
闭包让函数"携带"外部变量
defer 在函数返回前执行,多个 defer 是 LIFO 顺序
✅ defer 的参数立即求值,但函数体延迟执行


📂 配套资源