第 4 章:函数¶
将逻辑组织为可复用的单元 —— 掌握 Go 的函数声明、多返回值、闭包、defer 等核心特性。
4.1 函数声明¶
Go 使用 func 关键字声明函数,基本格式为:
一个最简单的函数示例:
package main
import "fmt"
// add 接受两个 int 参数,返回它们的和
func add(a int, b int) int {
return a + b
}
func main() {
result := add(3, 5)
fmt.Println(result) // 8
}
参数类型简写: 当连续多个参数具有相同类型时,可以省略前面参数的类型,仅在最后一个参数处标注:
4.2 多返回值¶
Go 函数允许返回 任意数量 的返回值。这是 Go 区别于大多数 C 系语言的显著特性之一。
// divide 返回商和余数
func divide(a, b int) (int, int) {
quotient := a / b
remainder := a % b
return quotient, remainder
}
func main() {
q, r := divide(17, 5)
fmt.Printf("17 / 5 = %d 余 %d\n", q, r) // 17 / 5 = 3 余 2
}
多返回值最常见的用途是 返回结果和错误:
import (
"fmt"
"strconv"
)
func main() {
// strconv.Atoi 返回两个值:转换后的整数和错误信息
n, err := strconv.Atoi("42")
if err != nil {
fmt.Println("转换失败:", err)
return
}
fmt.Println("转换成功:", n)
}
最佳实践: 在 Go 中,错误总是作为最后一个返回值。这一约定贯穿整个标准库和主流第三方库。
4.3 命名返回值¶
Go 允许为返回值指定 变量名。命名返回值在函数体中如同普通变量,无需在 return 语句中显式指明返回值:
// divide 使用命名返回值
func divide(a, b int) (quotient, remainder int) {
quotient = a / b // 直接对命名返回值赋值
remainder = a % b
return // 裸返回,自动返回 quotient 和 remainder
}
命名返回值的特性:
- 命名返回值在函数开始时会被自动初始化为 零值
- 使用命名返回值时,
return可以省略操作数,称为 裸返回(naked return) - 裸返回仅在短函数中推荐使用;长函数中使用裸返回会降低代码可读性
// 命名返回值自动初始化为零值
func split(sum int) (x, y int) {
x = sum / 2
y = sum - x
return
}
func main() {
fmt.Println(split(17)) // 8 9
}
4.4 可变参数¶
可变参数函数可以接受 任意数量 的同一类型参数,通过 ... 操作符实现:
// sum 接受任意数量的 int 参数
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
func main() {
fmt.Println(sum(1, 2)) // 3
fmt.Println(sum(1, 2, 3, 4)) // 10
fmt.Println(sum()) // 0(零个参数也可以)
}
内部机制: 可变参数在函数内部被转换为对应类型的 切片(slice)。上述 nums ...int 在函数内部等价于 nums []int。
传递切片给可变参数: 使用 slice... 语法将切片展开传入:
规则: 可变参数必须放在参数列表的 最后 一个位置。
// 正确:最后一个是可变参数
func format(prefix string, items ...int) string { ... }
// 编译错误:可变参数必须在最后
// func invalid(items ...int, suffix string) string { ... }
4.5 defer 延迟执行¶
defer 是 Go 中一个独特的关键字,用于 延迟一个函数调用 到当前函数返回之前执行。
4.5.1 基本用法¶
4.5.2 LIFO 顺序¶
多个 defer 按照 后进先出(LIFO, Last In First Out) 的顺序执行:
func main() {
defer fmt.Println("first") // 第三个执行
defer fmt.Println("second") // 第二个执行
defer fmt.Println("third") // 第一个执行
}
// 输出:
// third
// second
// first
LIFO 顺序与栈的弹栈顺序一致——最后一次 defer 注册的函数最先执行。
4.5.3 常见用途:资源清理¶
defer 最主要的用途是确保资源被正确释放,即使函数中途发生了错误或提前返回:
import (
"fmt"
"os"
)
func readFile(filename string) {
// 打开文件
file, err := os.Open(filename)
if err != nil {
fmt.Println("打开文件失败:", err)
return
}
// 注册关闭操作,在函数返回前执行
defer file.Close()
// 读取文件内容...
fmt.Println("正在读取文件:", filename)
// file.Close() 会在函数返回时自动调用
}
defer 的其他常见用途:
- 关闭网络连接、数据库连接
- 解锁互斥锁(
sync.Mutex) - 记录函数执行时间、打印进入和离开日志
4.5.4 参数求值时机¶
defer 的参数在声明时就已经求值,而非在函数执行时:
func main() {
x := 10
defer fmt.Println("defer 中的 x:", x) // x 在此时是 10
x = 20
fmt.Println("修改后的 x:", x)
}
// 输出:
// 修改后的 x:20
// defer 中的 x:10
这个行为是很多初学者的混淆点。如果希望在 defer 执行时捕获最新值,应使用闭包:
func main() {
x := 10
defer func() {
fmt.Println("defer 闭包中的 x:", x) // 引用 x,取最终值
}()
x = 20
fmt.Println("修改后的 x:", x)
}
// 输出:
// 修改后的 x:20
// defer 闭包中的 x:20
4.6 匿名函数与闭包¶
4.6.1 匿名函数¶
Go 支持 匿名函数——没有函数名的函数字面量,可以直接赋值给变量或立即调用:
func main() {
// 将匿名函数赋值给变量
add := func(a, b int) int {
return a + b
}
fmt.Println(add(3, 4)) // 7
// 定义并立即调用(IIFE 模式)
result := func(a, b int) int {
return a * b
}(3, 4)
fmt.Println(result) // 12
}
4.6.2 闭包¶
闭包(closure) 是一个函数值,它 捕获并记住了其外部作用域中的变量,即使该外部函数已经返回:
// 计数器生成器
func counter() func() int {
count := 0
return func() int {
count++ // 捕获外部变量 count
return count
}
}
func main() {
c1 := counter()
fmt.Println(c1()) // 1
fmt.Println(c1()) // 2
fmt.Println(c1()) // 3
// c2 拥有独立的 count
c2 := counter()
fmt.Println(c2()) // 1(c2 的 count 从 0 开始)
}
闭包的核心特性:
- 闭包捕获的是 变量的引用,而非变量的副本
- 每次调用外层函数都会创建新的闭包实例,各自拥有独立的环境
- 闭包的生命周期可能长于外层函数——只要闭包还在使用,被捕获的变量就不会被回收
4.6.3 闭包的常见陷阱¶
循环中捕获循环变量的值是一个经典问题:
func main() {
var funcs []func()
for i := 0; i < 3; i++ {
funcs = append(funcs, func() {
fmt.Println(i) // 捕获的是变量 i 的引用,而非当前值
})
}
for _, f := range funcs {
f()
}
}
// 输出(Go 1.22 之前):3 3 3
// 输出(Go 1.22 之后):0 1 2
说明: Go 1.22 版本改变了 for 循环变量的语义——每次迭代都会创建新的循环变量实例。在 Go 1.22 之前,所有闭包共享同一个循环变量,需要手动创建副本(i := i)来避免该问题。
4.7 函数作为值和参数¶
在 Go 中,函数是 一等公民(first-class citizen)——函数可以作为值赋值给变量,也可以作为参数传递给其他函数,还可以作为返回值返回。
// 接受一个函数参数
func applyOperation(a, b int, op func(int, int) int) int {
return op(a, b)
}
func add(a, b int) int { return a + b }
func mul(a, b int) int { return a * b }
func main() {
fmt.Println(applyOperation(3, 4, add)) // 7
fmt.Println(applyOperation(3, 4, mul)) // 12
fmt.Println(applyOperation(3, 4, func(a, b int) int {
return a - b
})) // -1(传入匿名函数)
}
函数类型: 在 Go 中,函数也是一种类型,完整的函数类型签名包括参数类型和返回值类型。
// 定义一个函数类型
type operation func(int, int) int
// 使用自定义函数类型作为参数
func applyOp(a, b int, op operation) int {
return op(a, b)
}
4.8 init 函数¶
init 函数是 Go 中一种 特殊的函数,由运行时在 main 函数之前自动调用:
var dbName string
// init 函数在包初始化时自动执行
func init() {
dbName = "my_database.db"
fmt.Println("init 1:数据库名称已设置")
}
// 一个包中可以有多个 init 函数,按照出现顺序执行
func init() {
fmt.Println("init 2:第二个 init 执行")
}
func main() {
fmt.Println("main 函数执行,dbName =", dbName)
}
// 输出:
// init 1:数据库名称已设置
// init 2:第二个 init 执行
// main 函数执行,dbName = my_database.db
init 函数的重要规则:
init函数 不接受参数,也不返回任何值init函数由运行时自动调用,不可显式调用- 一个包中可以定义 多个
init函数,按代码中出现顺序执行 - 不同包的
init函数按 依赖关系 确定执行顺序——被导入的包先初始化 init常用于设置初始状态、注册驱动、解析配置等需要在main之前完成的操作
包的初始化顺序:
- 导入依赖的包(递归初始化其依赖,再执行该包的初始化)
- 声明包级别的变量(按声明顺序执行变量初始化表达式)
- 执行
init函数(按源文件编译顺序)
4.9 本章小结¶
- 函数使用
func关键字声明,支持连续参数的类型简写 - 多返回值是 Go 函数的标志性特性,通常将错误作为最后一个返回值
- 命名返回值可以简化
return语句,但裸返回仅在短函数中推荐使用 - 可变参数使用
...操作符,函数内部作为切片处理,必须是参数列表的最后一个参数 defer延迟执行注册的函数,按 LIFO 顺序执行,最常用于资源释放和锁的解锁- 匿名函数和闭包允许函数捕获外部变量,每个外层函数调用创建独立的闭包环境
- 函数在 Go 中是一等公民,可以作为值、参数和返回值使用
init函数在main之前自动执行,用于包级别的初始化工作
实践任务¶
- 编写一个函数
swap(a, b int) (int, int),利用多返回值交换两个整数,并在main中调用 - 编写一个可变参数函数
average(nums ...float64) float64,计算任意数量浮点数的平均值 - 写一个文件操作函数,使用
defer确保文件被关闭(可使用os.Create创建临时文件) - 使用闭包实现斐波那契数列生成器:每次调用返回数列的下一个值
- 验证 Go 1.22 循环变量语义:在
for i := 0; i < 5; i++中创建 5 个闭包,观察i的最终值 - 创建一个包级别变量并通过
init函数初始化它,验证init在main之前执行