Go 函数、闭包与泛型
# Go 函数、闭包与泛型
掌握函数值、闭包、可变参数、多返回值、defer 和泛型的实际用法,并能判断什么时候该使用接口、什么时候该使用类型参数。
# 先记住一句话
函数值用于注入一段行为,闭包是函数与它引用的外部变量,泛型通过类型参数让同一份代码安全处理多种具体类型。
需要复用一段逻辑
├─ 只替换一个行为步骤 → 传入函数值
├─ 行为还要记住少量配置 → 使用闭包
├─ 同一套处理逻辑要适配多种类型 → 使用泛型
└─ 调用方只关心对象能力 → 定义小接口
2
3
4
5
# 函数也是一种值
函数可以赋值给变量、作为参数传入,也可以从另一个函数返回。真实项目中的 HTTP 中间件、重试回调、排序规则和依赖注入都大量使用这个特性。
// 文件位置:internal/note/filter.go
package note
type Note struct {
ID string // 笔记的唯一标识。
Published bool // 是否已经发布,true 表示允许筛选结果保留它。
}
// type 可以为函数签名命名;Predicate 描述 “输入一篇笔记,返回是否保留” 的行为契约。
type Predicate func(Note) bool
// keep 是调用方传入的筛选函数;它接收一篇 Note,并返回是否保留。
func Filter(notes []Note, keep Predicate) []Note {
// make 创建长度为 0、容量预留为 len(notes) 的切片,减少 append 扩容。
result := make([]Note, 0, len(notes))
// range 遍历切片;_ 明确忽略索引,只把当前元素赋给 current。
for _, current := range notes {
// keep(current) 为 true 时保留当前笔记,为 false 时跳过。
if keep(current) {
result = append(result, current)
}
}
return result
}
func PublishedOnly(current Note) bool {
// 直接返回 Published 字段,使这个函数满足 Predicate 的函数签名。
return current.Published
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
调用方可以传入命名函数 PublishedOnly,也可以传匿名函数。参数和返回值共同构成函数类型的一部分;两个签名不同的函数不能直接互换。
# 闭包会捕获外部变量
// 文件位置:internal/note/access.go
package note
// OwnerFilter 返回的函数会记住 ownerID,因此它是一个闭包。
func OwnerFilter(ownerID string) func(string) bool {
// 返回一个匿名函数;即使 OwnerFilter 已经执行结束,匿名函数仍会记住 ownerID。
return func(candidateOwnerID string) bool {
// candidateOwnerID 是以后调用闭包时传入的值,ownerID 是创建闭包时保存的值。
return candidateOwnerID == ownerID
}
}
2
3
4
5
6
7
8
9
10
11
闭包常用于包装配置、依赖或局部状态,但被捕获变量的生命周期可能延长。多个 Goroutine 同时读写同一个被捕获变量时,它仍是共享内存,必须使用锁、原子操作或重新设计所有权。
不要用闭包偷偷制造全局状态
闭包让变量看起来很局部,但只要返回的函数仍被持有,被捕获的数据就仍然存活。缓存、连接和可变 Map 应有明确所有者与关闭方式,不能仅因为写在函数内部就默认安全。
# 多返回值、可变参数和 defer
Go 常用 (value, error) 返回结果,也可以用第二个布尔值表达查询是否命中。可变参数在函数内部表现为切片。
// 文件位置:examples/function_features.go
package functionfeatures
import (
"errors" // 创建带说明文字的普通错误。
"strings" // 提供字符串拼接等工具函数。
)
// ...string 是可变参数,调用方可传任意数量的字符串,函数内把它当作 []string。
func JoinNonEmpty(separator string, parts ...string) string {
// 预留与 parts 相同的容量;实际长度先设为 0,因为空字符串不会加入结果。
filtered := make([]string, 0, len(parts))
// 逐个检查调用方传入的字符串。
for _, part := range parts {
if part != "" {
filtered = append(filtered, part) // 只保留非空字符串。
}
}
// Join 使用 separator 连接筛选后的字符串,例如分隔符为 "," 时得到 "a,b"。
return strings.Join(filtered, separator)
}
// First 返回切片中的第一个字符串;切片为空时返回错误,避免访问 parts[0] 发生越界 panic。
func First(parts []string) (string, error) {
if len(parts) == 0 {
// Go 常用零值与 error 一起返回;nil 表示没有错误。
return "", errors.New("parts cannot be empty")
}
return parts[0], nil // 成功时返回第一个元素;nil 表示没有错误。
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# defer 到底推迟了什么
defer 常用于解锁、关闭文件和结束 Trace。一个函数中有多个 defer 时,它们会在函数返回前按照后进先出的顺序执行。
需要特别区分两个时点:执行 defer 语句时,立即计算并保存被调用的函数和参数;等当前函数即将返回时,才真正调用这个函数。
// 文件位置:examples/defer_arguments.go
package functionfeatures
import "fmt"
func PrintSnapshot() {
value := 1
// 此时不会打印,但会立即读取 value,并把参数值 1 保存下来。
defer fmt.Println("defer:", value)
value = 2
fmt.Println("函数内部:", value)
// 函数即将结束时,执行之前推迟的 fmt.Println("defer:", 1)。
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
执行顺序是:
value := 1
↓
遇到 defer,保存参数值 1,但暂时不调用 Println
↓
value = 2
↓
先打印 “函数内部: 2”
↓
函数即将返回,再打印 “defer: 1”
2
3
4
5
6
7
8
9
因此输出为:
函数内部: 2
defer: 1
2
如果希望函数返回时再读取变量的最新值,可以使用闭包:
func PrintLatest() {
value := 1 // 这是闭包将要捕获的外部变量。
// defer 推迟匿名函数的执行;匿名函数没有普通参数,所以此处不会保存 value 的当前值。
defer func() {
fmt.Println("defer:", value) // 真正执行闭包时才读取 value,因此打印 2。
}()
value = 2 // 修改的是闭包捕获的同一个变量。
}
2
3
4
5
6
7
8
9
10
可以简单记成:defer 推迟的是调用动作,不是普通参数的计算;普通参数立即保存,闭包捕获的变量会在闭包真正执行时读取。
# 泛型解决什么问题
泛型允许函数或类型声明类型参数。类型参数仍受约束,编译器会检查调用是否合法,而不是把所有值都退化成 any 再做运行时断言。
// 文件位置:internal/collection/map.go
package collection
// 函数名后面的 [Input any, Output any] 是泛型的类型参数列表。
// Input 和 Output 只是自定义名称,作用类似 TypeScript 泛型中常用的 T 和 R,并非 Go 固定写法。
// Input 代表输入元素的类型,Output 代表转换后元素的类型;any 是类型约束,表示不额外限制类型。
func MapSlice[Input any, Output any](
// items 是待处理的输入切片,其中每个元素都是 Input 类型。
items []Input,
// transform 是调用方传入的转换函数:接收一个 Input,返回一个 Output。
transform func(Input) Output,
) []Output {
// 返回值需要与输入元素一一对应,因此创建相同长度的 Output 切片。
result := make([]Output, len(items))
// index 是元素位置,item 是当前位置的 Input 值。
for index, item := range items {
// 调用 transform 转换当前元素,再把结果放回相同位置,以保持原顺序。
result[index] = transform(item)
}
// result 的类型是 []Output。
return result
}
func LanguageNameLengths() []int {
languages := []string{"Go", "Python"}
// 调用时没有显式写 MapSlice[string, int],编译器会根据参数推断泛型类型:
// Input 是 string,Output 是 int。
// transform 接收一个语言名称,并返回它的长度,最终结果是 []int{2, 6}。
return MapSlice(languages, func(name string) int {
return len(name)
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
这次调用把 []string 转成了 []int。MapSlice 不关心具体类型和转换规则,只负责按照原顺序逐个取值、调用 transform,再收集结果;真正的转换规则由调用方传入的函数决定。
// 文件位置:internal/pagination/page.go
package pagination
// Page 用同一个分页结构承载不同领域类型,同时保留具体元素类型。
type Page[Item any] struct {
// 反引号中的 json 标签控制字段编码名;omitempty 会在空值时省略字段。
Items []Item `json:"items"` // 当前页的数据;元素类型由 Item 决定。
Total int `json:"total"` // 满足查询条件的数据总数。
NextCursor string `json:"nextCursor,omitempty"` // 下一页游标;空字符串时不输出该 JSON 字段。
}
2
3
4
5
6
7
8
9
10
any 等价于 interface{},表示不限制类型集合。需要操作符时,要声明更具体的约束。
// 文件位置:internal/number/sum.go
package number
// Number 的 ~ 允许底层类型为这些整数或浮点数的自定义类型进入集合。
type Number interface {
// | 表示类型集合的并集;~int 还允许底层类型为 int 的自定义类型。
~int | ~int64 | ~float64
}
func Sum[Value Number](values []Value) Value {
// Value 必须属于 Number 类型集合,因此 values 只能是允许的数字切片。
var total Value // 未显式赋值时获得 Value 对应的零值,允许作为累加起点。
for _, value := range values {
total += value // Number 约束保证 + 运算合法。
}
return total // 返回类型仍是调用方传入的具体数字类型,不会退化成 any。
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
如果没有 ~,自定义类型 type Score int 不属于只写 int 的类型集合;~int 表示底层类型为 int 的所有类型。
# 泛型、接口和 any 怎样选择
| 需求 | 更合适的工具 | 例子 |
|---|---|---|
| 同一算法处理多种类型并保留类型关系 | 泛型 | MapSlice[T, R]、Page[T] |
| 调用方只需要对象具备某种行为 | 接口 | io.Reader、Repository |
| 值的类型本来就不固定 | any 加显式检查 | 解码任意 JSON |
| 运行时检查结构和标签 | 反射 | JSON 编码器、依赖注入框架 |
泛型不应被用来消灭所有重复代码。如果两个函数只是代码长得像,但业务语义、错误处理和未来变化方向不同,强行抽成泛型会让接口更难理解。
# 一个完整的组合示例
下面把泛型函数和闭包组合起来:通用函数负责转换,闭包负责记住业务配置。
// 文件位置:examples/note_titles.go
package notetitles
import (
"fmt" // 按格式拼接标题前缀和正文。
"strings" // 清理标题两侧的空白字符。
)
type Note struct {
ID string // 笔记唯一标识。
Title string // 可能带有首尾空白的原始标题。
}
// MapSlice 把 []Input 中的每个元素转换为 Output,并按原顺序返回 []Output。
func MapSlice[Input any, Output any](items []Input, transform func(Input) Output) []Output {
result := make([]Output, len(items)) // 提前创建与输入等长的结果切片。
for index, item := range items {
result[index] = transform(item) // 使用调用方传入的函数转换当前元素。
}
return result
}
// titleFormatter 根据 prefix 创建一个标题格式化函数。
func titleFormatter(prefix string) func(Note) string {
return func(current Note) string {
// 闭包记住 prefix,调用时只需要传入当前笔记。
// TrimSpace 去掉标题首尾空白,Sprintf 再把前缀与清理后的标题拼起来。
return fmt.Sprintf("%s%s", prefix, strings.TrimSpace(current.Title))
}
}
// FormatTitles 把 Note 切片转换成格式化后的标题字符串切片。
func FormatTitles(notes []Note) []string {
// titleFormatter("笔记:") 先创建闭包,再由 MapSlice 对每篇 Note 调用它。
return MapSlice(notes, titleFormatter("笔记:"))
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
编译器通常能从 notes 和 titleFormatter 推断 Input 与 Output,所以调用时不必显式写成 MapSlice[Note, string](...)。
# 高频面试题与回答
1. 闭包是什么,有什么风险?参考答案
闭包是函数与它引用的外部变量。它适合保存少量配置并生成回调,但会延长被捕获变量的生命周期;多个 Goroutine 共享修改捕获变量时仍可能产生数据竞争。
2. defer 的参数什么时候求值?参考答案
执行到 defer 语句时就会计算被调用函数和参数,真正的函数调用则推迟到外层函数返回前,并按后进先出的顺序执行。
3. 泛型和接口有什么区别?参考答案
泛型主要表达多种类型之间的编译期关系,让同一算法保留具体类型;接口表达对象具备哪些行为,让调用方依赖能力而不是实现。真实项目可以同时使用,例如泛型容器中存放满足某个接口约束的类型。
4. 类型约束中的波浪号有什么作用?参考答案
~int 表示底层类型为 int 的所有类型,不仅包括预声明类型 int,也包括 type Score int 这样的自定义类型。它让约束按底层类型接受一组类型。
# 接下来学什么
下一篇学习 Go Struct、方法、接口与错误,把函数与类型的方法集、接口契约和错误链串起来。