
函数
前面我们已经学习了 Go 的变量、常量、数据类型、输入输出、条件控制、切片、字符串、映射表、指针和结构体。接下来开始学习 Go 语言中组织程序逻辑的核心单位:函数(function)。
很多初学者会把函数理解成"把几行代码包起来,之后方便重复调用"。这个理解没错,但还不完整。按照 Go 官方语言规范:Function declarations 的定义,函数声明会把一个名字绑定到一个函数上;函数的签名同时规定了它接收什么参数、返回什么结果。
简单理解:函数先定义边界,再承载逻辑;参数和返回值是边界,函数体是实现。
Go 函数还有一个很重要的特点:函数本身也是值。它可以赋值给变量、作为参数传给其他函数,也可以作为返回值返回。这让 Go 可以用很直接的方式实现回调、闭包、策略选择和中间件链。
本章按照"官方定义 → 基本声明 → 参数和返回值 → 函数类型 → 匿名函数与闭包 → defer → panic/recover → 泛型高阶函数 → 函数组合实现"的顺序展开,重点学习:
- 函数声明、调用和签名;
- 多返回值与 error 错误处理;
- 值传递、可变参数和参数作用域;
- 函数类型、函数值和方法表达式;
- 匿名函数与闭包;
- defer 的执行时机和后进先出规则;
- panic、recover 与正常错误返回的边界;
- 使用泛型函数实现 Map、Filter、Reduce;
- 使用函数类型实现一个可组合的中间件处理链。
本文代码在 go1.27.0 darwin/arm64 环境中实际编译运行。代码中的运行结果来自当前环境,地址、时间和系统相关输出可能有所不同。
函数的基本声明
函数使用 func 关键字声明:
Go
func add(a int, b int) int {
return a + b
}
其中:
- add 是函数名;
- a int, b int 是参数列表;
- 最后的 int 是返回值类型;
- return a + b 把计算结果返回给调用者。
如果相邻参数的类型相同,可以合并书写:
Go
func add(a, b int) int {
return a + b
}
这两种写法完全等价。
函数也可以没有参数或返回值:
Go
func printWelcome() {
fmt.Println("欢迎学习 Go")
}
func reset() {
// 没有参数,也没有返回值
}
**函数的签名由参数类型和返回值组成,参数名通常不影响函数类型。**比如下面两个函数的函数类型都是 func(int, int) int:
Go
func add(a, b int) int {
return a + b
}
func sum(x, y int) int {
return x + y
}
调用函数
调用函数时,在函数名后面写括号并传入参数:
Go
package main
import "fmt"
func add(a, b int) int {
return a + b
}
func main() {
result := add(10, 20)
fmt.Println("结果:", result)
}
运行结果:
结果: 30
Go 不允许调用参数数量或类型不匹配的函数:
Go
// add(10) // 编译错误:缺少参数
// add("10", 20) // 编译错误:第一个参数不是 int
这种错误在编译阶段就会被发现。函数签名越清楚,调用者越不容易误用。
参数和返回值
多个返回值
Go 函数可以返回多个值,这也是 Go 错误处理风格的基础:
Go
package main
import "fmt"
func divide(a, b int) (int, error) {
if b == 0 {
return 0, fmt.Errorf("除数不能为 0")
}
return a / b, nil
}
func main() {
result, err := divide(10, 2)
if err != nil {
fmt.Println("计算失败:", err)
return
}
fmt.Println("结果:", result)
_, err = divide(10, 0)
if err != nil {
fmt.Println("计算失败:", err)
}
}
运行结果:
结果: 5
计算失败: 除数不能为 0
Go 官方文档推荐把可以预期、可以由调用者处理的失败作为 error 返回,而不是随意使用 panic。调用者通过 if err != nil 判断错误,再决定重试、返回、记录日志或显示提示。
忽略返回值
如果只需要多个返回值中的一部分,可以使用空白标识符 _:
Go
result, _ := divide(10, 2)
fmt.Println(result)
_ 表示明确放弃这个值。对于错误返回值,不应该为了省事而无条件忽略:
cpp
// result, _ := divide(10, 0) // 可能掩盖真正的错误
只有在你确定错误不需要处理,或者函数契约已经保证不会失败时,忽略返回值才是合理的。
命名返回值
返回值可以在函数签名中声明名字:
Go
func splitName(fullName string) (first, last string) {
parts := strings.SplitN(fullName, " ", 2)
first = parts[0]
if len(parts) == 2 {
last = parts[1]
}
return
}
命名返回值会在进入函数时自动初始化为零值。裸 return 会返回当前这些变量的值。
完整示例:
Go
package main
import (
"fmt"
"strings"
)
func splitName(fullName string) (first, last string) {
parts := strings.SplitN(fullName, " ", 2)
first = parts[0]
if len(parts) == 2 {
last = parts[1]
}
return
}
func main() {
first, last := splitName("Rose Tea")
fmt.Println("first:", first)
fmt.Println("last:", last)
}
运行结果:
first: Rose
last: Tea
命名返回值在返回值较多、需要配合 defer 修改结果时很方便。但如果函数很长,多个变量都叫 result、err,裸 return 反而会让读者不容易看出返回了什么。因此,短函数可以使用,复杂函数更推荐显式 return value, err。
参数传递和作用域
Go 的普通函数参数是值传递。调用函数时,参数值会被赋给函数参数变量:
Go
package main
import "fmt"
func change(value int) {
value = 100
}
func main() {
number := 10
change(number)
fmt.Println(number)
}
运行结果:
10
change 修改的是自己的参数副本,所以 main 中的 number 仍然是 10。
如果希望函数修改调用者的变量,可以传入指针:
Go
func change(value *int) {
*value = 100
}
number := 10
change(&number)
fmt.Println(number) // 100
这里要注意,Go 并没有"引用传递"这个独立的参数机制。即使传入的是 *int,传递的仍然是一个指针值的副本;只不过两个指针副本指向同一个整数变量,所以解引用后可以修改原变量。
切片、map 和 channel 参数
切片、map 和 channel 的值包含对底层数据或运行时对象的引用。函数参数仍然按值传递,但函数可能通过这个引用修改底层内容:
Go
package main
import "fmt"
func changeFirst(numbers []int) {
numbers[0] = 99
}
func appendNumber(numbers []int) {
numbers = append(numbers, 100)
}
func main() {
numbers := []int{1, 2, 3}
changeFirst(numbers)
appendNumber(numbers)
fmt.Println(numbers)
}
运行结果:
[99 2 3]
changeFirst 修改了切片底层数组中的元素,所以调用者能看到 99。appendNumber 只修改了函数内部的切片描述符;追加后的长度没有返回给调用者,因此 main 仍然只能看到长度为 3 的切片。
如果函数需要改变切片长度,应当返回新切片:
cpp
func appendNumber(numbers []int) []int {
return append(numbers, 100)
}
numbers = appendNumber(numbers)
这比无条件传递 *\[\]int 更容易理解,也更符合 Go 的常见写法。
局部变量作用域
函数参数和函数体内声明的变量都属于局部变量,只在当前函数作用域内有效:
Go
func calculate() int {
base := 10
return base * 2
}
// fmt.Println(base) // 编译错误:base 不在当前作用域
如果函数内部声明了和外层同名的变量,内层变量会遮蔽外层变量:
Go
package main
import "fmt"
var name = "package"
func main() {
name := "local"
if true {
name := "block"
fmt.Println(name)
}
fmt.Println(name)
fmt.Println(getName())
}
func getName() string {
return name
}
运行结果:
block
local
package
短变量声明 := 只能在函数内部使用,并且至少要声明一个新的变量。理解作用域,可以避免在 if err := ... 中误以为修改了外层 err。
可变参数函数
如果函数最后一个参数前面加上 ...,它就可以接收数量不固定的参数:
Go
package main
import "fmt"
func sum(numbers ...int) int {
total := 0
for _, number := range numbers {
total += number
}
return total
}
func main() {
fmt.Println(sum())
fmt.Println(sum(1, 2, 3))
numbers := []int{4, 5, 6}
fmt.Println(sum(numbers...))
}
运行结果:
0
6
15
在函数体内,numbers 的类型是 \[\]int。调用时可以传入零个、一个或多个 int,也可以使用 slice... 把一个切片展开为可变参数。
可变参数只能放在参数列表的最后:
Go
// func invalid(values ...int, name string) {} // 编译错误
fmt.Println 等函数以及内置的 append 都使用了类似的可变参数设计。
函数类型和函数值
按照 Go 官方语言规范:Function types 的定义,函数类型由参数列表和结果列表决定:
Go
func(int, int) int
func(string) error
func()
函数变量可以保存符合类型的函数:
Go
package main
import "fmt"
func add(a, b int) int {
return a + b
}
func main() {
var operation func(int, int) int
operation = add
fmt.Println(operation(10, 20))
}
运行结果:
30
也可以使用类型定义让函数签名更容易阅读:
Go
type Operation func(int, int) int
var operation Operation = add
函数变量的零值是 nil。直接调用 nil 函数会产生运行时 panic:
Go
var operation func(int, int) int
fmt.Println(operation(1, 2)) // panic: 调用 nil 函数
调用前可以先判断:
Go
if operation != nil {
fmt.Println(operation(1, 2))
}
函数值不能和另一个函数值比较。函数只能和 nil 比较:
Go
// operation1 == operation2 // 编译错误:函数只能与 nil 比较
函数作为参数
函数可以作为参数传入另一个函数,这种函数通常称为高阶函数:
Go
package main
import "fmt"
func calculate(a, b int, operation func(int, int) int) int {
return operation(a, b)
}
func main() {
add := func(a, b int) int {
return a + b
}
multiply := func(a, b int) int {
return a * b
}
fmt.Println("加法:", calculate(3, 4, add))
fmt.Println("乘法:", calculate(3, 4, multiply))
}
运行结果:
加法: 7
乘法: 12
calculate 不关心具体是加法还是乘法,它只要求调用者提供一个符合 func(int, int) int 签名的函数。这就是把"变化的策略"作为参数传入。
函数作为返回值
函数也可以返回另一个函数:
Go
package main
import "fmt"
func makeAdder(base int) func(int) int {
return func(value int) int {
return base + value
}
}
func main() {
add10 := makeAdder(10)
add100 := makeAdder(100)
fmt.Println(add10(5))
fmt.Println(add100(5))
}
运行结果:
15
105
makeAdder(10) 返回的函数记住了 base = 10;makeAdder(100) 返回的函数记住了另一个 base。这种"函数携带它创建时周围变量"的行为,就是闭包的基础。
匿名函数和闭包
没有名字的函数叫函数字面量或匿名函数:
Go
double := func(value int) int {
return value * 2
}
fmt.Println(double(5))
匿名函数可以直接调用:
Go
result := func(a, b int) int {
return a + b
}(3, 4)
这里最后的 (3, 4) 是调用动作。如果只写到 },得到的是一个函数值;加上参数列表后,才是立即执行这个匿名函数。
闭包保存外部变量
闭包可以引用外层函数的变量。按照官方规范,这些变量会和闭包共享,并且在仍然可访问时继续存活:
Go
package main
import "fmt"
func counter() func() int {
count := 0
return func() int {
count++
return count
}
}
func main() {
first := counter()
second := counter()
fmt.Println(first())
fmt.Println(first())
fmt.Println(second())
fmt.Println(first())
}
运行结果:
1
2
1
3
first 和 second 是两个独立的闭包,各自保存自己的 count 。闭包非常适合实现计数器、缓存、配置函数和带状态的回调。
闭包与 goroutine
如果闭包在 goroutine 中执行,还需要考虑并发访问同一个变量的安全性:
Go
// 多个 goroutine 同时修改 count 可能产生数据竞争
count := 0
for i := 0; i < 10; i++ {
go func() {
count++
}()
}
闭包能延长变量生命周期,但不会自动提供锁,也不会自动解决数据竞争。并发场景需要使用 channel、sync.Mutex、atomic 等同步工具。这部分属于并发控制,不能因为代码写在闭包里就忽略。
defer:函数返回前执行清理
defer 会把一个函数调用推迟到当前函数返回之前执行。最常见的用途是关闭文件、释放锁、关闭响应体和恢复临时状态。
Go
package main
import "fmt"
func work() {
defer fmt.Println("清理资源")
fmt.Println("开始工作")
}
func main() {
work()
fmt.Println("函数已经返回")
}
运行结果:
开始工作
清理资源
函数已经返回
defer 注册的调用会在 work 返回前执行,所以清理动作紧挨着资源获取动作,代码不容易因为新增返回分支而漏掉清理。
多个 defer 的执行顺序
多个 defer 按照后进先出(LIFO)顺序执行:
Go
package main
import "fmt"
func main() {
defer fmt.Println("第一个注册")
defer fmt.Println("第二个注册")
defer fmt.Println("第三个注册")
fmt.Println("函数主体")
}
运行结果:
函数主体
第三个注册
第二个注册
第一个注册
可以把 defer 想象成一个栈:每执行一次 defer 就压入一个调用,函数返回时从栈顶依次弹出。
defer 的参数何时求值
defer 语句执行时,传给被延迟函数的参数就已经求值:
Go
package main
import "fmt"
func main() {
value := 1
defer fmt.Println("defer 看到的值:", value)
value = 2
fmt.Println("当前值:", value)
}
运行结果:
当前值: 2
defer 看到的值: 1
如果希望延迟执行时读取最新变量,可以把读取动作放进匿名函数:
Go
value := 1
defer func() {
fmt.Println(value)
}()
value = 2
这时匿名函数捕获的是变量本身,而不是在 defer 注册那一刻把当前值复制进参数。
panic 和 recover
panic 会停止当前函数的正常执行,并开始沿着当前 goroutine 的调用栈回退。回退过程中会执行已经注册的 defer。如果一直没有被 recover 捕获,程序最终会退出。
Go
package main
import "fmt"
func mustPositive(value int) {
if value <= 0 {
panic("value must be positive")
}
fmt.Println("合法值:", value)
}
func main() {
mustPositive(10)
// mustPositive(0) // panic: value must be positive
}
panic 适合表达"程序无法继续"或"违反了函数内部不变量"的情况,例如解析器内部发现不可能出现的状态。对普通业务错误,优先使用 error 返回值。
使用 recover 捕获 panic
recover 只有在 deferred 函数中直接调用时才有效:
Go
package main
import "fmt"
func safeDivide(a, b int) (result int, err error) {
defer func() {
if value := recover(); value != nil {
err = fmt.Errorf("计算失败: %v", value)
}
}()
return a / b, nil
}
func main() {
result, err := safeDivide(10, 2)
fmt.Println(result, err)
result, err = safeDivide(10, 0)
fmt.Println(result, err)
}
运行结果:
5 <nil>
0 计算失败: runtime error: integer divide by zero
这里使用命名返回值,是因为 deferred 函数需要在 panic 发生后修改 err。safeDivide 只是演示机制,真实代码中更推荐直接判断 b == 0 并返回一个明确的 error,这样不会把可预期错误变成 panic。
不要把 recover 当成通用错误处理
下面这种写法会把所有问题都吞掉:
Go
defer func() {
_ = recover()
}()
它可能隐藏 nil 指针、数组越界和其他程序错误。更合理的做法是只在明确的边界捕获 panic,例如服务器任务隔离、解析器内部实现,或者把一个内部 panic 转成对外的 error。
递归函数
函数可以调用自己,这叫递归。递归函数必须有明确的结束条件,否则会持续调用直到栈耗尽。
Go
package main
import "fmt"
func factorial(value int) int {
if value <= 1 {
return 1
}
return value * factorial(value-1)
}
func main() {
for i := 0; i <= 5; i++ {
fmt.Printf("%d! = %d\n", i, factorial(i))
}
}
运行结果:
Go
0! = 1
1! = 1
2! = 2
3! = 6
4! = 24
5! = 120
递归很适合树遍历、目录遍历、分治算法和解析表达式。但递归层数很深时会增加栈空间和调用成本,简单循环通常更直接。选择递归时,应先确认问题本身确实具有递归结构。
使用泛型函数处理集合
Go 1.18 引入泛型后,函数可以使用类型参数。泛型函数能够在保留类型安全的同时复用算法。
Map:转换每个元素
Map 接收一组 T,再接收一个把 T 转换成 R 的函数,最后返回一组 R:
Go
func Map[T any, R any](values []T, transform func(T) R) []R {
result := make([]R, len(values))
for index, value := range values {
result[index] = transform(value)
}
return result
}
Filter:保留满足条件的元素
Go
func Filter[T any](values []T, keep func(T) bool) []T {
result := make([]T, 0, len(values))
for _, value := range values {
if keep(value) {
result = append(result, value)
}
}
return result
}
Reduce:累积得到一个结果
Go
func Reduce[T any, R any](values []T, initial R, combine func(R, T) R) R {
result := initial
for _, value := range values {
result = combine(result, value)
}
return result
}
完整测试
Go
package main
import "fmt"
func Map[T any, R any](values []T, transform func(T) R) []R {
result := make([]R, len(values))
for index, value := range values {
result[index] = transform(value)
}
return result
}
func Filter[T any](values []T, keep func(T) bool) []T {
result := make([]T, 0, len(values))
for _, value := range values {
if keep(value) {
result = append(result, value)
}
}
return result
}
func Reduce[T any, R any](values []T, initial R, combine func(R, T) R) R {
result := initial
for _, value := range values {
result = combine(result, value)
}
return result
}
func main() {
numbers := []int{1, 2, 3, 4, 5}
squares := Map(numbers, func(value int) int {
return value * value
})
even := Filter(numbers, func(value int) bool {
return value%2 == 0
})
total := Reduce(numbers, 0, func(result, value int) int {
return result + value
})
fmt.Println("平方:", squares)
fmt.Println("偶数:", even)
fmt.Println("总和:", total)
}
运行结果:
平方: [1 4 9 16 25]
偶数: [2 4]
总和: 15
这三个函数展示了函数作为参数的实际价值:遍历逻辑由 Map、Filter、Reduce 负责,具体业务规则由调用者提供。它们不会修改传入的切片描述符长度,也不会修改元素本身,返回的是新的结果切片。
如果项目使用的是 Go 1.21 及以上版本,也可以查阅标准库的 slices 包。标准库已经提供了许多常见切片操作,业务代码应优先复用经过验证的实现,而不是为了练习而重复造轮子。
使用函数实现一个中间件处理链
前面我们已经把函数当成参数和返回值使用。接下来实现一个更接近实际项目的例子:由多个函数组成的处理链。
中间件通常有这样的结构:
Go
请求
│
▼
日志中间件 → 鉴权中间件 → 业务处理函数
│ │ │
└────── 返回响应结果 ◄─────────┘
定义请求、响应和函数类型
Go
type Request struct {
Path string
User string
Value int
}
type Response struct {
Status string
Value int
}
type Handler func(Request) Response
type Middleware func(Handler) Handler
Handler 表示一个"接收请求并返回响应"的函数;Middleware 表示一个"接收下一个 Handler,并返回包装后 Handler"的函数。
组合多个中间件
Go
func Chain(final Handler, middlewares ...Middleware) Handler {
handler := final
for index := len(middlewares) - 1; index >= 0; index-- {
handler = middlewares[index](handler)
}
return handler
}
为什么要从后向前组合?假设调用顺序希望是:日志 → 鉴权 → 业务,那么最内层应该先是业务,再包上鉴权,最后包上日志。从后向前包裹后,调用顺序才会和书写顺序一致。
编写日志和鉴权中间件
Go
func withLog(name string) Middleware {
return func(next Handler) Handler {
return func(request Request) Response {
fmt.Println(name, "开始:", request.Path)
response := next(request)
fmt.Println(name, "结束:", response.Status)
return response
}
}
}
func withAuth(next Handler) Handler {
return func(request Request) Response {
if request.User == "" {
return Response{Status: "401 unauthorized"}
}
return next(request)
}
}
withLog 是一个闭包:它记住了传入的 name,然后返回一个新的中间件函数。每个中间件都不需要修改全局变量,状态通过函数参数和闭包明确传递。
完整运行代码
Go
package main
import "fmt"
type Request struct {
Path string
User string
Value int
}
type Response struct {
Status string
Value int
}
type Handler func(Request) Response
type Middleware func(Handler) Handler
func Chain(final Handler, middlewares ...Middleware) Handler {
handler := final
for index := len(middlewares) - 1; index >= 0; index-- {
handler = middlewares[index](handler)
}
return handler
}
func withLog(name string) Middleware {
return func(next Handler) Handler {
return func(request Request) Response {
fmt.Println(name, "开始:", request.Path)
response := next(request)
fmt.Println(name, "结束:", response.Status)
return response
}
}
}
func withAuth(next Handler) Handler {
return func(request Request) Response {
if request.User == "" {
return Response{Status: "401 unauthorized"}
}
return next(request)
}
}
func business(request Request) Response {
return Response{
Status: "200 ok",
Value: request.Value * 2,
}
}
func main() {
handler := Chain(
business,
withLog("access"),
withAuth,
)
response := handler(Request{
Path: "/double",
User: "rose",
Value: 21,
})
fmt.Printf("响应: %+v\n", response)
response = handler(Request{Path: "/double", Value: 21})
fmt.Printf("未登录响应: %+v\n", response)
}
运行结果:
access 开始: /double
access 结束: 200 ok
响应: {Status:200 ok Value:42}
access 开始: /double
access 结束: 401 unauthorized
未登录响应: {Status:401 unauthorized Value:0}
这个例子把函数的几个能力组合到了一起:
- Handler 和 Middleware 是自定义函数类型;
- withLog 返回函数,并通过闭包保存日志名称;
- Chain 接收可变数量的中间件;
- 每个中间件只关心自己的规则,不需要知道其他中间件的实现;
- 最终处理逻辑 business 可以单独测试,也可以被不同链路复用。
这就是函数可组合性的实际价值:把变化点放进小函数里,再通过函数参数把它们拼成一条处理流程。
函数、方法和方法表达式
方法是带有接收者的函数。结构体方法的接收者写在 func 和方法名之间:
Go
type Counter struct {
Value int
}
func (counter *Counter) Add(value int) {
counter.Value += value
}
方法可以像普通方法调用一样使用:
Go
counter := Counter{}
counter.Add(3)
也可以使用方法表达式,把接收者变成普通函数的第一个参数:
Go
add := (*Counter).Add
counter := Counter{}
add(&counter, 3)
完整示例:
Go
package main
import "fmt"
type Counter struct {
Value int
}
func (counter *Counter) Add(value int) {
counter.Value += value
}
func main() {
counter := Counter{}
counter.Add(3)
add := (*Counter).Add
add(&counter, 4)
fmt.Println(counter.Value)
}
运行结果:
7
方法表达式适合把方法传给需要普通函数的 API,也可以帮助理解方法的本质:方法只是带有特殊接收者参数的函数。
常见错误和避坑提醒
误区一:所有错误都使用 panic
panic 会中断当前 goroutine 的正常流程。对于文件不存在、参数不合法、数据库暂时不可用等调用者可以处理的情况,应该返回 error。
误区二:以为函数参数是引用传递
Go 的参数始终是值传递。指针、切片、map 等参数之所以能影响外部数据,是因为复制后的值仍然指向相同的底层数据或变量。
误区三:忽略错误返回值
result, _ := operation() 会让错误消失。除非明确知道错误不重要,否则应该检查 err,或者把错误继续向上返回。
误区四:忘记调用返回的函数
Go
func makeTask() func() {
return func() { fmt.Println("执行任务") }
}
makeTask() // 只创建函数,没有执行任务
makeTask()() // 创建并立即执行
返回函数和调用返回的函数是两步动作,最后的 () 不能省略。
误区五:闭包共享变量却没有考虑并发
闭包会共享外部变量,但不提供并发安全保证。多个 goroutine 同时读写闭包变量时,需要使用同步工具。
误区六:在循环里无意中捕获同一个变量
闭包会捕获变量,而不是自动复制变量的当前值。Go 1.22 之后,for range 的迭代变量语义已经改进,但涉及普通 for 循环、指针或共享可变状态时,仍然应该明确每次闭包需要捕获的值,不能靠猜测。
误区七:滥用命名返回值和裸 return
命名返回值适合短小、变量含义明确的函数。函数太长时,裸 return 会隐藏真实返回内容,应该改成显式返回。
一份可以直接复制的综合示例
下面的例子把多返回值、闭包、可变参数和 defer 放在一起,模拟一个带统计信息的任务执行器:
Go
package main
import "fmt"
type Task func() error
func runTasks(tasks ...Task) (success, failed int) {
defer func() {
fmt.Printf("任务统计: 成功=%d 失败=%d\n", success, failed)
}()
for index, task := range tasks {
if err := task(); err != nil {
fmt.Printf("任务 %d 失败: %v\n", index+1, err)
failed++
continue
}
fmt.Printf("任务 %d 成功\n", index+1)
success++
}
return
}
func main() {
base := 10
addBase := func(value int) error {
if base+value < 0 {
return fmt.Errorf("结果不能为负数")
}
fmt.Println("计算结果:", base+value)
return nil
}
runTasks(
func() error { return addBase(5) },
func() error { return addBase(-20) },
func() error { return fmt.Errorf("模拟失败") },
)
}
运行结果:
计算结果: 15
任务 1 成功
任务 2 失败: 结果不能为负数
任务 3 失败: 模拟失败
任务统计: 成功=1 失败=2
这个示例说明:
- Task 是一个没有参数、返回 error 的函数类型;
- runTasks 使用可变参数接收任意数量的任务;
- addBase 是捕获 base 的闭包;
- 任务通过返回 error 表达失败,没有用 panic;
- 命名返回值让 defer 可以在函数结束时读取统计结果。
总结
本章我们学习了 Go 函数的完整基础:
- 函数使用 func 声明,由参数列表、返回值和函数体组成;
- 函数签名规定了调用者可以传入什么、得到什么;
- Go 支持多个返回值,error 通常作为最后一个返回值;
- Go 的参数传递始终是值传递,指针和引用型值只是让底层数据可以被共享;
- 可变参数在函数体内表现为切片,调用切片时可以使用 ... 展开;
- 函数也是值,可以赋值、传参和作为返回值;
- 匿名函数可以引用外层变量,这种行为形成闭包;
- 闭包会共享外部变量,但不会自动解决并发安全问题;
- defer 在当前函数返回前执行,多个 defer 按后进先出顺序运行;
- panic/recover 适合处理不可继续或明确隔离的异常,普通错误优先返回 error;
- 泛型函数可以抽象出 Map、Filter、Reduce 等可复用算法;
- 函数类型可以组合成中间件、回调、策略和任务处理链。
真正理解 Go 函数之后,就不会把它简单看成"重复执行的代码块"。更准确的理解是:函数用签名定义边界,用返回值传递结果,用闭包保存必要状态,再通过函数类型把小逻辑组合成可测试、可复用的处理流程。
官方资料: