Go 函数:类型、闭包、方法值、泛型与迭代器
面向有 Java 经验的开发者,基于 Go 1.26。
Go 函数不只是“把一段代码封装起来”。它有自己的静态类型,可以赋值、传参、返回并与 nil 比较;闭包能够捕获变量;方法可以转换成方法值或方法表达式;泛型函数可以把同一套算法用于一族类型。从 Go 1.23 开始,特定签名的函数甚至直接构成了 range 的迭代协议。
要把这些行为串起来,需要同时观察三个层次:源码中的声明与调用、类型系统中的签名,以及运行时的栈帧与闭包环境。后文也会沿着这三个层次展开。
目录
1. 先建立心智模型
先把几个容易混在一起的概念拆开:
- 函数声明在包级别定义一个有名字的函数;
- 函数字面量在表达式位置创建匿名函数;
- 函数类型由参数列表和结果列表决定;
- 闭包是函数值加上它所引用的外部变量环境;
- 方法是带 receiver 的函数声明;
- 调用会先求出被调用函数和实参,再建立本次调用的参数变量。
func Add(a, b int) int { // 函数声明
return a + b
}
type Operation func(int, int) int // 定义函数类型
var op Operation = Add // 函数值
offset := 10
addOffset := func(n int) int { // 函数字面量;同时形成闭包
return n + offset
}函数类型的零值是 nil。函数值只能拿来与 nil 比较,彼此之间不能比较,也不能作为 map key。
var fn func(int) int
fmt.Println(fn == nil) // true
// fn(1) // panic:调用 nil 函数2. 函数声明与签名
2.1 基本语法
func Divide(dividend int, divisor int) (int, int) {
return dividend / divisor, dividend % divisor
}相邻参数类型相同时可合并:
func Divide(dividend, divisor int) (quotient, remainder int) {
return dividend / divisor, dividend % divisor
}函数签名包含:
- 参数的数量、顺序和类型;
- 结果的数量、顺序和类型;
- 是否为可变参数;
- 若是泛型函数,还包括类型参数列表。
参数名和结果名不属于函数类型:
var f func(int, int) int
f = func(left, right int) int { return left + right }
f = func(x, y int) int { return x * y }2.2 无参数、无结果
func Flush() {
// ...
}Go 没有 void 类型。无结果函数就是结果列表为空,不能把调用结果赋值。
2.3 参数声明顺序
Go 写“名字在前、类型在后”,复杂类型从左向右仍容易读:
func Transform(
ctx context.Context,
input []byte,
apply func(context.Context, []byte) ([]byte, error),
) ([]byte, error)回调签名如果在多个 API 中出现,应定义命名类型:
type Transformer func(context.Context, []byte) ([]byte, error)命名类型可以承载文档,也可以定义方法;不过,它与底层相同的未命名函数类型在赋值规则上仍有细节。设计 API 时,不必为了“看起来正式”制造大量只用一次的名字。
3. 参数传递是值传递
Go 的参数一律按值传递。代码中表现出的“引用效果”,来自被复制的值内部仍然指向共享数据。
3.1 普通值
func Increment(n int) {
n++
}
n := 1
Increment(n)
fmt.Println(n) // 13.2 指针
func Increment(n *int) {
(*n)++
}
n := 1
Increment(&n)
fmt.Println(n) // 2传入函数的仍是指针值的副本,只是这个副本也能访问同一目标。函数重新给自己的指针参数赋值,并不会改变调用者变量中保存的指针。
3.3 slice
slice 值包含指向底层数组的指针、长度和容量。传参会复制这三个字段:
func SetFirst(values []int) {
values[0] = 99 // 修改共享底层数组
}
func AppendLocal(values []int) {
values = append(values, 4) // 只更新局部 slice header
}若函数要让调用者看到新长度,返回 slice:
func AppendValue(values []int, value int) []int {
return append(values, value)
}
values = AppendValue(values, 4)3.4 map、channel、function 和 interface
这些值复制后通常仍引用同一个运行时对象或动态值,但仍然是值传递。把 map 参数赋成新 map 不会替换调用者变量;向 map 写 entry 会修改共享表。
所以,“复制后仍然引用同一个运行时对象”比“Go 有些类型按引用传递”更准确。后者很容易让人误判重新赋值、nil 和并发行为。
4. 多返回值与错误
4.1 多返回值不是 tuple 对象
func Lookup(key string) (Value, bool)
func Parse(text string) (Value, error)调用时多重赋值:
value, ok := Lookup("language")
if !ok {
return ErrNotFound
}多返回值只能用于特定语法位置,不能作为一个可传递的 tuple 值:
// pair := Lookup("language") // 编译错误:多值不能赋给单个变量需要长期保存或跨 API 传递,就定义 struct:
type LookupResult struct {
Value Value
Found bool
}4.2 error 通常放最后
func ReadConfig(path string) (Config, error)成功返回可用值和 nil,失败返回类型零值与非 nil error:
func ParsePort(text string) (uint16, error) {
n, err := strconv.ParseUint(text, 10, 16)
if err != nil {
return 0, fmt.Errorf("解析端口 %q: %w", text, err)
}
return uint16(n), nil
}有时即使 error 非 nil,前面的结果仍有意义,例如 io.Reader.Read 可能同时返回 n > 0 和 io.EOF。是否能使用部分结果由具体 API 契约决定,不能机械套“err 非 nil 就丢掉结果”。
4.3 直接返回与增加上下文
if err := repository.Save(user); err != nil {
return fmt.Errorf("保存用户 %q: %w", user.ID, err)
}
return nil只在当前层能增加有用上下文时包装。每层都重复“failed to”会形成冗长而无信息的错误链。
5. 命名返回值
结果可以命名:
func Split(total int) (left, right int) {
left = total / 2
right = total - left
return
}命名结果在函数入口处声明并初始化为零值,可以被 defer 读取和修改。裸 return 返回它们当前的值。
5.1 适合的场景
- 返回值含义无法从类型看出,如
(width, height int); - defer 需要合并关闭错误;
- 短函数中名称显著改善文档。
func WriteFile(path string, data []byte) (err error) {
file, err := os.Create(path)
if err != nil {
return err
}
defer func() {
err = errors.Join(err, file.Close())
}()
_, err = file.Write(data)
return err
}5.2 不适合的场景
长函数里大量修改命名结果,再用裸 return,会让读者难以确认实际返回值:
func calculate(...) (result Result, err error) {
// 两百行后 return,result 和 err 在哪里被改过很难追踪
return
}可以保留结果名用于文档,但显式返回:
return result, err结果名与局部变量同处函数体作用域,短声明时还要防止遮蔽。
6. 函数是一等值
6.1 赋值与传参
type Predicate[T any] func(T) bool
func Filter[T any](values []T, keep Predicate[T]) []T {
result := make([]T, 0, len(values))
for _, value := range values {
if keep(value) {
result = append(result, value)
}
}
return result
}调用:
positive := Filter(numbers, func(n int) bool {
return n > 0
})回调适合表达局部策略。跨多个方法、带状态和生命周期的能力更适合接口。
6.2 返回函数
func Multiplier(factor int) func(int) int {
return func(value int) int {
return value * factor
}
}
double := Multiplier(2)
fmt.Println(double(21)) // 42返回的闭包在 Multiplier 返回后仍能使用 factor。编译器会确保被捕获状态的生命周期足够长,必要时让它逃逸到堆。
6.3 函数值的限制
- 只能与 nil 比较;
- 不能作为 map key;
- 调用 nil 函数会 panic;
- 函数值可能携带闭包环境,不应假设它只是一个机器代码地址;
- 标准库没有稳定方式取得函数值的语义身份。
需要注册和注销回调时,应返回显式 token 或 cancel 函数,而不是比较函数值:
cancel := bus.Subscribe(handler)
defer cancel()7. 匿名函数与闭包
7.1 立即调用
result := func(a, b int) int {
return a + b
}(20, 22)立即调用可以建立局部作用域,但常规流程若不需要隔离 defer 或复杂初始化,直接写普通代码更简单。
7.2 捕获变量,不是快照
count := 0
next := func() int {
count++
return count
}
fmt.Println(next()) // 1
fmt.Println(next()) // 2闭包引用变量本身,后续赋值可见:
prefix := "dev"
format := func(name string) string {
return prefix + ":" + name
}
prefix = "prod"
fmt.Println(format("api")) // prod:api若需要创建时的快照,把值作为参数传入工厂函数,或复制到新的不会再修改的变量。
7.3 多个闭包共享状态
func Counter() (next func() int, reset func()) {
count := 0
return func() int {
count++
return count
}, func() {
count = 0
}
}两个闭包共享同一个 count。这是一种封装状态的方式,但并不自动并发安全。
8. 闭包捕获与并发
8.1 Go 1.22 的循环变量
对语言版本 Go 1.22 及以上,循环用 := 声明的变量每轮都是新变量:
for _, item := range items {
go func() {
process(item)
}()
}每个闭包捕获对应迭代的 item。但如果循环使用 = 写入外层变量,它仍是共享变量:
var item Item
for _, item = range items {
go func() {
process(item) // 共享外层 item
}()
}语义由模块语言版本决定,通常看 go.mod 的 go 行。
8.2 捕获安全不等于数据安全
counter := 0
for range 10 {
go func() {
counter++ // 数据竞争
}()
}闭包共享 counter,必须同步。可用 mutex、atomic,或让一个 goroutine 独占状态。运行:
go test -race ./...8.3 生命周期和取消
启动 goroutine 的函数必须说明它何时结束:
func Start(ctx context.Context, interval time.Duration, fn func()) {
go func() {
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
fn()
case <-ctx.Done():
return
}
}
}()
}这里仍有两个 API 问题:调用者如何等待真正停止,以及 fn panic 怎么处理。工程接口可能返回 done <-chan struct{} 或 stop/wait 句柄。
9. 方法、方法值与方法表达式
9.1 方法声明
type Counter struct {
value int
}
func (c *Counter) Add(delta int) int {
c.value += delta
return c.value
}receiver 是一个额外的、写在函数名前的参数。方法属于定义在同一包中的非指针、非接口命名类型(以及符合规范的别名限制)。
9.2 方法值
counter := &Counter{}
add := counter.Add
fmt.Println(add(2))
fmt.Println(add(3))counter.Add 会求值并保存 receiver,得到类型 func(int) int。以后调用 add 不再提供 receiver。
receiver 在创建方法值时求值。值 receiver 会被复制到函数值中:
type Point struct{ X int }
func (p Point) GetX() int { return p.X }
p := Point{X: 1}
get := p.GetX
p.X = 2
fmt.Println(get()) // 1指针 receiver 保存的是指针副本,目标后续变化可见。
9.3 方法表达式
add := (*Counter).Add
counter := &Counter{}
fmt.Println(add(counter, 5))(*Counter).Add 得到普通函数,receiver 成为第一个显式参数,类型为:
func(*Counter, int) int值 receiver:
getX := Point.GetX
fmt.Println(getX(Point{X: 42}))方法表达式适合把某个方法作为通用算法传递,而不绑定具体实例。
9.4 自动取地址的边界
对可寻址值调用指针 receiver 方法时,编译器可自动取地址:
var c Counter
c.Add(1) // 等价于 (&c).Add(1)但 map 元素不可寻址:
counters := map[string]Counter{"a": {}}
// counters["a"].Add(1) // 编译错误需要取出、修改、写回,或 map 保存指针。方法值的创建同样受可寻址性和 method set 约束。
10. 可变参数
10.1 声明与调用
最后一个参数可写成 ...T:
func Sum(values ...int) int {
total := 0
for _, value := range values {
total += value
}
return total
}
Sum()
Sum(1, 2, 3)函数体中 values 的类型是 []int。
已有 slice 可展开:
numbers := []int{1, 2, 3}
total := Sum(numbers...)除特定内建函数外,不能把展开 slice 和额外同类型实参混写:
// Sum(0, numbers...) // 普通可变参数调用不允许这种混合
all := append([]int{0}, numbers...)
Sum(all...)10.2 是否复制
调用 Sum(numbers...) 时,函数收到的 slice 会共享调用者的底层数组;函数修改元素可能影响调用者:
func Zero(values ...int) {
for i := range values {
values[i] = 0
}
}
numbers := []int{1, 2, 3}
Zero(numbers...)
fmt.Println(numbers) // [0 0 0]若 API 不应修改输入,文档说明并遵守;需要保存参数到调用结束之后时,考虑 slices.Clone,避免调用者后续修改。
10.3 ...any 的代价
func Log(values ...any)它灵活,但失去元素类型约束,值装入接口可能带来转换和分配,也难以表达参数结构。结构化日志更适合明确的键值类型或属性类型;公共 API 不要把 ...any 当作重载替代品。
11. 递归
函数可以直接或间接调用自己:
func Factorial(n uint64) uint64 {
if n < 2 {
return 1
}
return n * Factorial(n-1)
}递归必须有可到达的终止条件,并考虑数值溢出。Go 的整数溢出不会自动变成 error。
11.1 栈会增长,但不是无限
goroutine 栈从较小尺寸开始并可增长,但深度失控仍会耗尽内存或超过运行时上限。Go 不保证尾调用优化:
func SumTail(values []int, acc int) int {
if len(values) == 0 {
return acc
}
return SumTail(values[1:], acc+values[0])
}不要假设它会变成常量栈。线性遍历优先写循环。
11.2 适合递归的结构
树、语法结构、分治算法用递归往往最清楚:
func Walk(node *Node, visit func(*Node) error) error {
if node == nil {
return nil
}
if err := visit(node); err != nil {
return err
}
for _, child := range node.Children {
if err := Walk(child, visit); err != nil {
return err
}
}
return nil
}处理不可信深度的输入时,使用显式栈并设置节点、深度和内存上限,避免拒绝服务。
11.3 递归闭包
函数字面量要递归调用自己,先声明变量:
var visit func(*Node)
visit = func(node *Node) {
if node == nil {
return
}
fmt.Println(node.Value)
for _, child := range node.Children {
visit(child)
}
}直接写 visit := func(...) { visit(...) } 时,右侧初始化期间 visit 尚未进入作用域。
12. 泛型函数
12.1 类型参数与约束
func Max[T cmp.Ordered](a, b T) T {
if a < b {
return b
}
return a
}T 是类型参数,cmp.Ordered 是约束。调用时通常能推断:
fmt.Println(Max(3, 5))
fmt.Println(Max("go", "java"))无法从普通参数推断时显式提供类型实参:
value := Zero[time.Duration]()12.2 any 与 comparable
func First[T any](values []T) (T, bool) {
var zero T
if len(values) == 0 {
return zero, false
}
return values[0], true
}
func Contains[T comparable](values []T, target T) bool {
for _, value := range values {
if value == target {
return true
}
}
return false
}any 不代表“运行时随便处理”,而是算法不需要对 T 做额外操作。要用 ==,约束必须允许比较;要用 <,需要相应类型集合。
12.3 底层类型近似
type Integer interface {
~int | ~int8 | ~int16 | ~int32 | ~int64
}
func Sum[T Integer](values []T) T {
var total T
for _, value := range values {
total += value
}
return total
}~int 包含底层类型为 int 的命名类型。没有 ~ 时只包含精确的预声明类型。
12.4 保留命名 slice 类型
func Map[S ~[]E, E any, R any](source S, fn func(E) R) []R {
result := make([]R, len(source))
for i, value := range source {
result[i] = fn(value)
}
return result
}如果输出元素仍是 E,可以让返回类型也是 S,从而保留调用者的命名 slice 类型:
func Clone[S ~[]E, E any](source S) S {
return append(S(nil), source...)
}API 设计要明确是否应该保留命名类型,而不是一味追求最复杂的签名。
12.5 何时不要用泛型
- 操作依赖行为而不是类型运算时,用接口;
- 只有一个具体类型的业务流程,不必泛化;
- 各类型分支完全不同,泛型不会消除 type switch;
- 复杂约束让调用点和错误信息比重复两段短代码更难懂。
泛型擅长容器、算法和类型安全的适配器,不是 Java 方法重载或继承体系的替代品。
13. 函数迭代器
Go 1.23 允许 range 三种函数签名:
func(func() bool)
func(func(V) bool)
func(func(K, V) bool)标准库 iter 定义了:
type Seq[V any] func(yield func(V) bool)
type Seq2[K, V any] func(yield func(K, V) bool)13.1 一个可组合的迭代器
func MapSeq[A, B any](source iter.Seq[A], transform func(A) B) iter.Seq[B] {
return func(yield func(B) bool) {
for value := range source {
if !yield(transform(value)) {
return
}
}
}
}使用:
lengths := MapSeq(slices.Values(words), func(word string) int {
return len(word)
})
for length := range lengths {
fmt.Println(length)
}13.2 yield 的协议
迭代器作者必须遵守:
- 每次调用 yield 最多产生一组值;
- yield 返回 true 才能继续;
- yield 返回 false 后不得再次调用;
- 迭代结束就返回;
- 资源清理应覆盖调用者提前停止的路径。
func Lines(scanner *bufio.Scanner) iter.Seq2[string, error] {
return func(yield func(string, error) bool) {
for scanner.Scan() {
if !yield(scanner.Text(), nil) {
return
}
}
if err := scanner.Err(); err != nil {
yield("", err)
}
}
}这个设计把错误作为最后一项,调用者容易忘记处理。实践中也可以让迭代器只产出值,并由拥有 scanner 的对象提供 Err();选择哪种形式应保持整个库一致。
13.3 iter.Pull
next, stop := iter.Pull(sequence)
defer stop()
value, ok := next()Pull 形式适合 zip、归并和需要同时推进多个序列的算法。只要可能提前结束,就调用 stop。不要并发调用 next 或 stop,除非具体 API 文档明确允许。
13.4 迭代器与所有权
迭代器可能返回指向内部缓冲区的 slice 或指针,下一次迭代就覆盖。API 必须说明值是否稳定。调用者需要长期保存时应复制:
for record := range records {
saved = append(saved, bytes.Clone(record))
}懒迭代减少中间 slice,但增加生命周期和错误传播复杂度。小集合上直接返回 slice 往往更简单。
14. 逃逸分析、内联与性能
14.1 栈和堆由编译器决定
Go 不按 new、& 或闭包语法简单决定分配位置。若一个值的生命周期可能超过当前调用,或分析无法证明它留在栈上,它可能逃逸到堆:
func NewCounter() *int {
n := 0
return &n
}返回局部变量地址是安全的,编译器会保证它的生命周期。
查看诊断:
go test -gcflags='all=-m=2' ./...
go build -gcflags='all=-m=2' ./...这些输出属于编译器诊断,不是稳定 API,会随着版本和代码形态变化。
14.2 闭包可能分配,也可能不分配
立即调用且不逃逸的闭包通常可以在栈上处理;闭包被返回、存入堆对象或交给 goroutine 时,它的环境更可能逃逸。不要因为“闭包一定慢”就提前改写,先用:
go test -bench=. -benchmem观察 allocs/op,再结合 profile 和逃逸输出定位。
14.3 内联
编译器能把符合条件的小函数内联到调用点,减少调用开销,并为常量传播、边界检查消除创造机会。能否内联由编译器预算和实现决定,不能作为 API 契约。
清晰的小函数通常不会成为性能问题。为了避开一个可能早已被内联的调用而复制业务逻辑,损失的是确定的可维护性。只有 benchmark 确认这里是热点,才值得根据当前工具链的诊断继续优化。
14.4 函数值和接口调用
直接调用、函数值调用和接口方法调用的优化机会并不相同,编译器有时可以去虚拟化并内联,有时则不能。间接调用确实不利于分支预测和内联,但业务系统的主要成本往往在分配、锁和 I/O。最终仍要用真实负载测量。
14.5 参数大小
值传递大 struct 可能产生复制;改成指针可能减少复制,却带来:
- nil 状态;
- 可变共享与数据竞争;
- 更复杂的所有权;
- 逃逸和 GC 压力;
- 较差的缓存局部性。
值还是指针,应当先由语义决定,再到真实热点中测量。小而不可变的值类型通常很适合值传递。
15. API 与工程设计
15.1 参数少而明确
参数持续增加,通常意味着缺少领域对象,或者函数承担了太多职责。不要把所有参数都塞进无类型的 map[string]any,更清楚的做法是定义配置 struct:
type ClientOptions struct {
Timeout time.Duration
MaxRetries int
Transport http.RoundTripper
}构造选项稳定且字段多时可以用 struct。functional options 适合可选配置和需要校验/默认值的构造器,但会增加间接层,不是每个构造器都需要。
15.2 接受接口,返回具体类型
这条经验的核心是让函数只依赖所需能力:
func Decode(r io.Reader) (*Document, error)接口最好由使用方定义,并保持小。返回具体类型让调用者能使用完整能力;当隐藏实现或允许多个实现确实是 API 目标时,返回接口也合理。
15.3 context 的位置
按 Go 惯例,context.Context 是第一个参数,名为 ctx,不存入 struct,不作为可选 nil:
func Fetch(ctx context.Context, id string) (Record, error)context 表达请求生命周期、取消和截止时间,不用于塞普通可选业务参数。
15.4 谁创建,谁负责清理
返回资源时明确所有权:
func Open(path string) (io.ReadCloser, error)调用者看到 io.Closer 应知道要关闭。若函数内部只临时创建资源,就在函数内 defer。回调式 API 可以把资源生命周期限制在回调内,但要清楚回调不能保存资源供之后使用。
15.5 panic 边界
普通库函数用 error 报告可预期失败。panic 适合内部不变量被破坏、程序无法继续的情况。若 API 接受用户回调,要说明回调 panic 是否传播;默认传播通常比静默吞掉更易诊断。只有 goroutine 或插件边界确实需要隔离时才 recover,并转换成有上下文的失败。
16. 与 Java 的对照
| Java | Go | 关键差异 |
|---|---|---|
| 方法 / static 方法 | 函数 / 方法 | Go 包级函数不依附 class |
| overload | 无函数重载 | 用不同名字、泛型或不同类型建模 |
| checked exception | error 返回值 | 错误是普通值 |
| lambda | 函数字面量 | Go 闭包捕获变量,可读写 |
| functional interface | 函数类型 | 不需单方法接口即可传行为 |
method reference obj::m | 方法值 obj.M | receiver 已绑定 |
method reference Type::m | 方法表达式 Type.M | receiver 是首个参数 |
varargs T... | ...T | 函数体中是 slice,可用 slice... 展开 |
| generic method | 泛型函数/泛型类型的方法 | Go 方法不能自行声明额外类型参数 |
| Stream | iter.Seq + range | 更小的语言级 push 协议 |
| JVM 栈 | 可增长 goroutine 栈 | 仍无尾调用优化保证 |
Go 没有默认参数和重载。为了模拟重载而使用 ...any 加 type switch,通常会损失编译期检查。优先给行为不同的函数不同名字,或用配置 struct、泛型表达真正统一的算法。
17. Go 1.22–1.26 相关变化
- Go 1.22:按模块语言版本启用每轮独立的循环变量;闭包捕获 range/for 声明变量的常见问题得到语义层修复。加入整数 range。
- Go 1.23:加入 range-over-function 和标准库
iter,函数签名正式成为语言迭代协议;slices、maps中多个 API 与迭代器组合。 - Go 1.24–1.26:函数声明、闭包、方法值、可变参数和泛型函数的上述核心规则没有需要改写的语法变化。工具链的内联、逃逸和去虚拟化会持续改进,但这些属于实现,不应写成源码正确性依赖。
版本说明必须区分语言特性、标准库 API 和编译器优化。某段代码在 Go 1.26 分配更少,不代表规范保证未来始终如此。
18. 常见误区
- 说 Go “有引用传递”:所有参数都是值传递;共享效果来自值中包含指针或运行时引用。
- 修改 slice 参数的 header,期待调用者长度变化:返回新 slice 并赋回。
- 把多返回值当 tuple 保存:需要保存就定义 struct。
- 长函数使用裸 return:结果从哪里赋值难以追踪。
- 比较两个函数值:函数只能与 nil 比较。
- 调用 nil 函数:会 panic,回调可选时先判断。
- 认为闭包捕获创建时快照:闭包捕获变量。
- 认为 Go 1.22 后所有闭包并发都安全:共享对象和外层变量仍需同步。
- 混淆方法值与方法表达式:前者绑定 receiver,后者把 receiver 变成首参。
- 认为值 receiver 方法值会追踪原变量:创建时 receiver 值已复制。
- 认为
slice...一定复制:展开传递共享底层数组。 - 假设尾递归优化:Go 不保证,应对深线性递归使用循环。
- 用
any约束后执行==:需要comparable。 - 迭代器在 yield false 后继续:违反协议。
- 凭感觉优化闭包和小函数:用 benchmark、profile 与
-m=2。
19. 速查表
func F(a int, b string) (Result, error)
func F(a, b int) int
func F(values ...int) int
func F[T comparable](a, b T) bool
type Handler func(context.Context, Request) (Response, error)匿名函数和闭包:
fn := func(value int) int { return value * 2 }
func Factory(offset int) func(int) int {
return func(value int) int { return value + offset }
}方法值与方法表达式:
bound := object.Method // receiver 已绑定
bound(arg)
unbound := (*Type).Method
unbound(object, arg)泛型:
func Contains[S ~[]E, E comparable](values S, target E) bool迭代器:
func Values[T any](items []T) iter.Seq[T] {
return func(yield func(T) bool) {
for _, item := range items {
if !yield(item) {
return
}
}
}
}诊断:
go test -race ./...
go test -bench=. -benchmem ./...
go build -gcflags='all=-m=2' ./...20. 练习
- 实现
Map[S ~[]E, E, R any]和Filter[S ~[]E, E any],思考返回值是否应保留命名 slice 类型。 - 写一个返回
next、value、reset三个闭包的计数器,然后用 race detector 证明它默认不适合并发;再加 mutex。 - 为一个值 receiver 和一个指针 receiver 分别创建方法值,创建后修改原变量,记录调用结果并解释 receiver 的求值时机。
- 实现可变参数函数
Concat(separator string, parts ...string),测试零参数、直接实参和 slice 展开。明确函数是否修改parts。 - 用递归遍历树,再用显式栈改写。加入最大深度限制,比较错误处理和遍历顺序。
- 实现
Reduce[S ~[]E, E, A any]。解释为什么累加器类型 A 可能与元素类型 E 不同。 - 实现
Take[T any](iter.Seq[T], n int) iter.Seq[T],确保够 n 项后停止上游,调用方提前 break 时也停止。 - 用
iter.Pull实现两个有序 Seq 的归并,保证所有返回路径调用 stop。 - 对直接循环、传入函数值、接口方法三种转换策略做 benchmark,并查看
-m=2;记录当前工具链结果,但不要把它当语言保证。 - 实现写文件函数,用命名 error 和 defer 合并
Close错误。分别模拟写失败、关闭失败和两者同时失败。
21. 可运行示例
函数不仅是可复用代码块,也是可以捕获状态、绑定 receiver、接受类型参数的值。下面三个例子位于 examples/ch07。
21.1 两个闭包共享一份词法状态
闭包保存的是可继续访问的词法环境。 外层函数返回后,局部变量为什么还能存在?不同闭包和不同函数调用之间怎样共享状态?
package main
import "fmt"
// counter 返回两个共享同一份局部状态的闭包。
// next 修改 value,current 只读取 value;只要闭包仍可达,这份状态就必须保持有效。
// 状态最终放在栈还是堆属于编译器实现细节,不是语言层面的承诺。
func counter(start int) (next func() int, current func() int) {
value := start
next = func() int {
value++
return value
}
current = func() int {
return value
}
return next, current
}
func main() {
next, current := counter(10)
fmt.Printf("初始=%d\n", current())
fmt.Printf("next=%d\n", next())
fmt.Printf("next=%d\n", next())
fmt.Printf("当前=%d\n", current())
// 另一次调用拥有独立的词法环境,不会与上面的计数器共享状态。
otherNext, _ := counter(100)
fmt.Printf("另一个=%d\n", otherNext())
}运行:
go run ./examples/ch07/closure-state预期输出:
初始=10
next=11
next=12
当前=12
另一个=101两组计数器揭示了共享边界。
next和current都引用同一次counter调用中的 value,因此一个闭包修改后,另一个能读到新值。- 外层函数返回不代表 value 立即销毁;只要闭包仍可达,它捕获的环境就必须继续存活。
- 第二次调用
counter(100)会创建独立环境,不与第一个计数器共享 value。 - 这份可变状态没有同步保护,多 goroutine 并发调用会数据竞争;闭包不会自动提供线程安全。
把共享扩展到并发场景。 再创建一个读取第一个计数器的闭包,验证共享;然后写两个 goroutine 调 next,用 go run -race 观察竞态诊断。
21.2 方法值何时绑定 receiver
receiver 在方法值创建时就会绑定。 object.Method 保存的究竟是对象快照还是对象引用?它与 Type.Method 有什么区别?
package main
import "fmt"
type Counter struct {
Value int
}
func (counter Counter) Snapshot() int {
return counter.Value
}
func (counter *Counter) Increment() int {
counter.Value++
return counter.Value
}
func main() {
counter := Counter{Value: 10}
// 创建方法值时 receiver 会被求值:
// 值 receiver 复制当前 Counter;指针 receiver 保存 &counter。
snapshot := counter.Snapshot
increment := counter.Increment
counter.Value = 20
fmt.Printf("值 receiver 的快照=%d\n", snapshot())
fmt.Printf("指针 receiver 的结果=%d\n", increment())
fmt.Printf("原变量现在=%d\n", counter.Value)
// 方法表达式不绑定 receiver,调用时必须显式传入。
read := Counter.Snapshot
fmt.Printf("方法表达式=%d\n", read(counter))
}运行:
go run ./examples/ch07/method-value预期输出:
值 receiver 的快照=10
指针 receiver 的结果=21
原变量现在=21
方法表达式=21值 receiver 与指针 receiver 从这里分开。
- 创建
snapshot := counter.Snapshot时,值 receiver 被复制,所以后来把原变量改成 20,不影响快照里的 10。 increment := counter.Increment绑定的是指向原变量的指针,调用后原变量从 20 变成 21。Counter.Snapshot是方法表达式,没有绑定任何对象,调用者必须显式传入 receiver。- 把方法当回调传递前,要先确定希望冻结当下值,还是继续操作同一个可变对象。
改变绑定时机再比较。 在创建两个方法值之前先把 counter 改为 20,比较结果;再写 (*Counter).Increment 方法表达式并显式传 &counter。
21.3 泛型函数分离元素与结果类型
泛型参数把输入、元素和结果分别建模。 怎样写一次 Map/Reduce,同时保留编译期类型检查,并允许命名切片类型作为输入?
package main
import "fmt"
// Map 让输入元素类型 E 和输出元素类型 R 相互独立。
// ~[]E 还允许 MySlice 这类底层类型为 []E 的命名切片作为输入。
func Map[S ~[]E, E, R any](source S, transform func(E) R) []R {
result := make([]R, len(source))
for index, value := range source {
result[index] = transform(value)
}
return result
}
// Reduce 的累加器 A 不必与元素 E 同类型,例如可以把整数归约为字符串。
func Reduce[S ~[]E, E, A any](source S, initial A, combine func(A, E) A) A {
accumulator := initial
for _, value := range source {
accumulator = combine(accumulator, value)
}
return accumulator
}
type Scores []int
func main() {
scores := Scores{10, 20, 30}
labels := Map(scores, func(score int) string {
return fmt.Sprintf("%d分", score)
})
total := Reduce(scores, 0, func(sum, score int) int {
return sum + score
})
fmt.Printf("labels=%v\n", labels)
fmt.Printf("total=%d\n", total)
}运行:
go run ./examples/ch07/generic-functions预期输出:
labels=[10分 20分 30分]
total=60三个类型参数各有自己的职责。
S ~[]E表示 S 的底层类型是[]E,所以命名类型Scores也能传入。- Map 的 E 与 R 独立,可以把 int 转换成 string,而不局限于同类型变换。
- Reduce 的累加器 A 与元素 E 独立;本例相同都是 int,但也可以把元素归约成结构体或文本。
- result 由函数新建并交给调用方,和 source 不共享底层数组,后续修改 labels 不会影响 scores。
换一种结果类型验证抽象。 用 Map 把分数映射为 bool(是否及格);再用 Reduce 把分数组合成逗号分隔文本,体会 A 与 E 不同的价值。
22. 官方资料
- Go 语言规范:Function declarations
- Go 语言规范:Function types
- Go 语言规范:Function literals
- Go 语言规范:Calls
- Go 语言规范:Method declarations
- Go 语言规范:Method expressions
- Go 语言规范:Method values
- Go 语言规范:Type parameter declarations
- Go 语言规范:For statements with range clause
iter包cmp包- Go 1.22 Release Notes
- Go 1.23 Release Notes
- Go 1.26 Release Notes
- Go Wiki:Compiler And Runtime Optimizations
- Effective Go:Functions
函数设计的核心不是把代码切成更多小块,而是让类型、所有权、失败方式和生命周期一眼可见。语法只是入口;真正稳定的 API 会让调用者很难误用。