GO [ 函数 ]

函数

前面我们已经学习了 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}

这个例子把函数的几个能力组合到了一起:

  1. Handler 和 Middleware 是自定义函数类型;
  2. withLog 返回函数,并通过闭包保存日志名称;
  3. Chain 接收可变数量的中间件;
  4. 每个中间件只关心自己的规则,不需要知道其他中间件的实现;
  5. 最终处理逻辑 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 函数的完整基础:

  1. 函数使用 func 声明,由参数列表、返回值和函数体组成;
  2. 函数签名规定了调用者可以传入什么、得到什么;
  3. Go 支持多个返回值,error 通常作为最后一个返回值;
  4. Go 的参数传递始终是值传递,指针和引用型值只是让底层数据可以被共享;
  5. 可变参数在函数体内表现为切片,调用切片时可以使用 ... 展开;
  6. 函数也是值,可以赋值、传参和作为返回值;
  7. 匿名函数可以引用外层变量,这种行为形成闭包;
  8. 闭包会共享外部变量,但不会自动解决并发安全问题;
  9. defer 在当前函数返回前执行,多个 defer 按后进先出顺序运行;
  10. panic/recover 适合处理不可继续或明确隔离的异常,普通错误优先返回 error;
  11. 泛型函数可以抽象出 Map、Filter、Reduce 等可复用算法;
  12. 函数类型可以组合成中间件、回调、策略和任务处理链。

真正理解 Go 函数之后,就不会把它简单看成"重复执行的代码块"。更准确的理解是:函数用签名定义边界,用返回值传递结果,用闭包保存必要状态,再通过函数类型把小逻辑组合成可测试、可复用的处理流程。

官方资料:

相关推荐
2601_966949651 小时前
每日自动更新股票行情:如何设计可靠的数据任务,避免重复写入和脏数据
开发语言·python·数据分析·pandas·量化交易·股票数据·quantdash
QuZhengRong1 小时前
【Spring】后端接收的请求参数多了一个逗号的处理办法
java·后端·spring
ZealSinger1 小时前
Go1.25容器感知GOMAXPROCS避限流
java·开发语言
打工仔折腾 AI1 小时前
从零写一个CAD 02:实体容器、Esc取消与键盘失灵的排查
人工智能·后端·python·性能优化
茉莉玫瑰花茶2 小时前
GO [ 类型 ]
开发语言·数据库·golang
用户8356290780512 小时前
使用 Python 拆分和提取 PDF 页面
后端·python
用户8356290780512 小时前
使用 Python 操作 PowerPoint 中的图片
后端·python
梅雅达编程笔记2 小时前
02_BeautifulSoup网页数据提取
开发语言·爬虫·python·beautifulsoup·数据采集
Escalating_xu2 小时前
【C 语言】深入理解指针(3):字符指针、数组指针、二维数组传参、函数指针与转移表
java·c语言·开发语言