4.1 学习 Go 函数:理解 Go 里的函数
函数是把一段可复用逻辑封装起来的代码单元。
本章说明:如何定义函数、参数与返回值、匿名函数与闭包、defer 延迟调用,以及 panic / recover 的异常处理。
一、函数是什么
- 按功能或逻辑拆分代码,有利于可读性与维护。
- Go 里常见两类写法:具名函数、匿名函数(无函数名)。
- Go 是编译型语言,函数定义顺序与能否被调用无关(不要求像部分脚本语言那样「先定义后调用」)。
二、函数的定义与声明
使用关键字 func,依次写:函数名、参数列表、返回值列表、函数体(花括号内)。
func 函数名(形式参数列表)(返回值列表) {
函数体
}- 形式参数:在函数内是局部变量,由调用方传入实参。
- 返回值列表:可写返回值的类型;若只有一个返回值且未命名,外层括号可省略;无返回值时可不写返回值列表。
示例:
package main
import "fmt"
func sum(a int, b int) int {
return a + b
}
func main() {
fmt.Println(sum(1, 2))
}相邻同类型参数可合并写法:func sum(a, b int) int。
三、参数
1. 普通参数
调用时个数、类型需与定义一致(无 Python 式的默认参数、**kwargs 等语法)。少传、多传或类型不对,编译器会报错。
package main
import "fmt"
// 多个不同类型参数
func greet(name string, age int) {
fmt.Printf("%s 今年 %d 岁\n", name, age)
}
// 相邻同类型可合并:a、b 都是 int
func rectArea(width, height int) int {
return width * height
}
func main() {
greet("小明", 18)
fmt.Println("面积:", rectArea(3, 4))
}2. 可变参数:...T
使用 ...类型 表示「零个或多个该类型实参」,在函数体内对应 []T 切片。可变参数必须是参数列表中的最后一个。
package main
import "fmt"
func sum(args ...int) int {
var s int
for _, v := range args {
s += v
}
return s
}
func main() {
fmt.Println(sum(1, 2, 3))
}3. 多类型可变参数:...interface{}
需要同时接收多种类型时,可用 ...interface{}(空接口,参见 相关章节),再按类型分支处理。标准库 fmt.Printf 即类似思路。
package main
import "fmt"
func MyPrintf(args ...interface{}) {
for _, arg := range args {
switch arg.(type) {
case int:
fmt.Println(arg, "is int")
case string:
fmt.Println(arg, "is string")
default:
fmt.Println(arg, "other")
}
}
}
func main() {
MyPrintf(1, "hello", 3.14)
}4. 展开切片:... 传参
调用可变参数函数时,可用 slice... 把切片拆开逐个传入,而不是把整个切片当作一个参数。
package main
import "fmt"
func sum(args ...int) int {
var r int
for _, v := range args {
r += v
}
return r
}
func SumAll(parts ...int) int {
return sum(parts...) // 等价于把 parts 中每个元素传给 sum
}
func main() {
fmt.Println(SumAll(1, 2, 3))
}四、返回值
1. 无返回值
不写返回值类型时,可用 return 提前结束,return 后不能带值。
package main
import "fmt"
func logOnly(msg string) {
fmt.Println(msg)
return // 可省略
}
func main() {
logOnly("hi")
}2. 多个返回值
Go 支持多返回值,常用于 result, err 模式。
package main
import "fmt"
func double(a int) (int, int) {
b := a * 2
return a, b
}
func main() {
x, y := double(2)
fmt.Println(x, y)
}3. 用 _ 忽略某个返回值
多返回值函数有时只需要其中一部分,另一部分可用空白标识符 _ 显式丢弃(与 for _, v := range 里忽略下标同理)。
package main
import "fmt"
func pair() (int, int) {
return 1, 2
}
func main() {
x, _ := pair() // 只要第一个返回值,第二个用 _ 忽略
fmt.Println(x) // 1
// 不能写成单变量接收多返回值,例如:
// s := pair() // 编译错误:multiple-value pair() in single-value context
}两个返回值都不需要时,可写 _, _ := pair()。
4. 命名返回值
在返回值列表中写出变量名,相当于在函数开头已声明;函数体内可直接赋值,裸 return 会返回这些命名变量(易读但也易让人忽略赋值,需留心)。
package main
import "fmt"
func doubleNamed(a int) (b int) {
b = a * 2
return // 等同于 return b
}
func main() {
fmt.Println(doubleNamed(2))
}五、匿名函数
没有函数名的函数,只有参数、返回值(可省略)与函数体:
func(参数列表)(返回参数列表) {
函数体
}典型用法:
- 定义后立即执行(末尾一对括号传入实参,常称 IIFE):
package main
import "fmt"
func main() {
func(data int) {
fmt.Println("hello", data)
}(100)
}- 赋给变量再调用:匿名函数本身是值,可存进变量,类型为
func(...),之后像普通函数一样多次调用。
package main
import "fmt"
func main() {
mul := func(a, b int) int {
return a * b
}
fmt.Println(mul(2, 3)) // 6
fmt.Println(mul(4, 5)) // 20
}- 作为值传递(回调、传给
go func()等):
package main
import "fmt"
func visit(list []int, f func(int)) {
for _, v := range list {
f(v)
}
}
func main() {
visit([]int{1, 2, 3, 4}, func(v int) {
fmt.Println(v)
})
}只在一处使用的短逻辑,用匿名函数可减少「再起一个包级函数名」的负担;需要多次复用同一段逻辑时,可先赋给局部变量(如上第 2 种)。
六、闭包
闭包:函数引用了外部作用域的变量,这些变量被「捕获」在函数闭包中,多次调用可共享、修改同一状态。
package main
import "fmt"
func adder() func(int) int {
sum := 0
return func(x int) int {
sum += x
return sum
}
}
func main() {
pos := adder()
fmt.Println(pos(1)) // 1
fmt.Println(pos(2)) // 3
fmt.Println(pos(3)) // 6
}注意: 若在循环里创建闭包并延迟异步使用(例如 go func()),勿直接捕获循环变量指针,应传参复制当前值,避免「全是最后一次循环的值」(详见并发章节常见写法)。
七、延迟调用 defer
defer 将函数调用推迟到当前函数返回之前执行(return 之后、真正返回给调用方之前,按栈处理)。
- 多个
defer:按 LIFO(后进先出) 执行。 defer后面的表达式会立即求值,只有被 defer 的函数调用在返回前执行。
package main
import "fmt"
func main() {
defer fmt.Println("world")
fmt.Println("hello")
}
// 输出:先 hello,再 world常见用途: 关闭文件、解锁互斥锁、Close() 资源等,与「成对操作」写在一起,减少遗漏。
package main
import (
"fmt"
"os"
)
func main() {
file, err := os.Open("example.txt")
if err!= nil {
fmt.Printf("无法打开文件: %v\n", err)
return
}
defer file.Close()
// 这里开始处理文件,例如读取文件内容
var content []byte
content, err = os.ReadFile("example.txt")
if err!= nil {
fmt.Printf("无法读取文件: %v\n", err)
return
}
fmt.Println(string(content))
}
defer的完整机制(执行顺序、LIFO、参数何时求值、资源清理、发生panic时是否仍会执行等)见 1.14 流程控制:defer 延迟语句。
八、panic 与 recover
Go 没有 try/catch。运行时不可恢复错误可用 panic(v) 中断当前 goroutine 的正常流程,并沿调用栈向上展开;若未被恢复,程序崩溃。
recover() 只能在 defer 的函数里调用:可拦截当前 goroutine 上的 panic,拿到 panic 传入的值,使程序从 panic 点恢复继续执行(通常用于必须兜底的库或 HTTP 中间件等)。
package main
import "fmt"
func safe() {
defer func() {
if r := recover(); r != nil {
fmt.Println("recovered:", r)
}
}()
panic("something wrong")
}
func main() {
safe()
fmt.Println("main continues")
}实践建议:
- 业务错误优先用
error返回值,不要滥用panic。 recover只应处理确实需要兜底的场景;滥用会掩盖 bug。
recover为何必须在defer里调用、defer与panic的配合及示例,见 《1.16 异常机制:panic 和 recover》
九、方法与函数
方法是绑定到类型(多为结构体)上的特殊函数,定义时带有接收者 func (r Receiver) Name(...)。普通函数无接收者。
方法与函数的对比及示例见 2.1 面向对象:结构体
小结
| 主题 | 要点 |
|---|---|
| 定义 | func + 名 + 参数 + 返回值 + 体;定义顺序不限 |
| 参数 | 可变参数 ...T、展开 slice...、多类型可用 ...interface{} |
| 返回值 | 多返回值、_ 忽略某项、命名返回值与裸 return |
| 匿名函数 | 立即执行(IIFE)、赋给变量再调用、作参数/回调 |
| 闭包 | 捕获外部变量;注意循环变量与异步 |
defer | 返回前执行、LIFO、常用于释放资源 |
panic/recover | 非通用异常机制;优先 error,recover 仅在 defer 中有效 |