Skip to content

Go 函数:看清参数何时传入,代码何时执行 ​

把函数传给另一个函数、返回一个闭包、在退出前执行清理,这几件事在 Go 里都很常见。难点往往不在语法,而在时间:函数拿到的是哪个值,闭包后来读到的是哪个变量,return 与 defer 谁先改结果。

这篇从普通调用开始,再把执行时间往后移,依次讲函数值、闭包和 defer。读完应该能判断一段函数代码的输出,也知道文件关闭、回调和循环里最容易出错的地方。示例实际运行于 Go 1.27.2、Linux/amd64;完整程序、测试和故意无法编译的反例放在文末。

先认清几种写法在做什么 ​

写法它表达什么典型用途
func divide(a, b int) (int, int)一个函数接收两个整数,返回两个整数同时返回商和余数
func(int) int函数类型:输入一个整数,输出一个整数保存或传递变换规则
func(n int) int { return n + 2 }函数字面量,产生一个函数值写一个短回调
func counter() func() int函数返回另一个函数把状态和后续操作放在一起
func total(xs ...int) int最后一个参数接收数量可变的整数聚合一组同类型参数
defer f(x)现在确定 f 和 x,退出当前函数前调用资源清理

这几种写法可以组合使用。比如 defer func() { ... }() 同时用了函数字面量、函数调用和延迟执行。先拆开各自的规则,比把整段代码背下来容易。声明、函数类型和调用规则以语言规范为准。[1]

多返回值:一次调用把相关结果交齐 ​

商和余数来自同一次除法,分别返回很自然:

go
func divmod(a, b int) (int, int) { return a / b, a % b }

q, r := divmod(17, 5)
fmt.Println(q, r)

实际输出:

text
3 2

(int, int) 是两个返回值,并没有在这里声明一个可以到处传递的“元组类型”。调用方用两个变量接住;确实不需要余数时,可以写 q, _ := divmod(17, 5)。同样的机制也用于 (value, error) 和 (value, bool)。前者通常让调用者处理失败,后者常用于表达“找到没有”;具体含义由函数的 API 约定,而非编译器替它决定。[2]

这个 divmod 为了集中演示返回值,要求 b != 0。整数除零会 panic;需要接收不可信输入的实际函数,应先验证除数,再选择返回错误或其他约定。别因为例子短,就把它当作完成了所有输入检查。[3]

命名返回值是给结果变量起名,例如 func bounds() (low, high int)。这些名字位于函数作用域内,进入函数时先取零值;没有表达式的 return 会返回它们当前的值。短函数里名字能说明两个整数各是什么,长函数里裸 return 却可能让读者到处找最后一次赋值。本文通常保留显式 return,只在需要演示结果变量如何被 defer 修改时使用它。[2:1]

函数也是值:把变化的规则传进去 ​

所谓“一等公民”,在这里有很具体的含义:函数值可以赋给变量、作为参数,也可以作为结果返回。函数类型描述参数和结果的类型;参数名不影响类型是否相同,是否为可变参数则会影响。[1:1]

go
type Transform func(int) int

func applyTwice(n int, f Transform) int { return f(f(n)) }

fmt.Println(applyTwice(3, func(n int) int { return n + 2 }))
text
7

applyTwice 负责“执行两次”,传入的函数负责“每次怎么变换”。这里先把 3 变成 5,再把 5 变成 7。排序时传比较函数、测试时传替代实现,也是在分开稳定的流程和变化的动作。对只有一个动作的需求,函数参数通常已经够清楚;需要几个共同操作或一个有明确身份的对象时,再考虑接口。这是组织代码的取舍,不是语言强制的设计模式。[1:2]

函数值有两个必须记住的边界:

操作是否允许原因或结果
var f func()允许零值为 nil
f == nil允许可以检查可选回调是否存在
调用 nil 的 f()编译允许,运行时 panic没有可调用的函数
f == g编译不通过函数值只能与 nil 比较
把 func(int) int 传给要求 func(int64) int64 的参数不会自动适配参数和结果类型不同

因此,回调是否可以为 nil,要在接口约定中讲清楚。若回调是必填参数,静默跳过它可能掩盖错误;若它是可选钩子,调用前检查 f != nil 就有意义。不要靠取函数代码地址来判断两个闭包“是不是同一个”;语言没有提供这种函数相等关系。[4]

普通函数调用按值传参。传入 slice 或指针时,复制的是 slice 值或指针值,它们仍可能指向调用者的数据。这一点到了可变参数和闭包里同样成立。[5]

闭包保留变量,后续调用会继续用它 ​

一个匿名函数使用外层作用域的变量,就能把那份状态留给后续调用:

go
func counter(start int) func() int {
    n := start
    return func() int { n++; return n }
}

a, b := counter(10), counter(10)
fmt.Println(a(), a(), b())
text
11 12 11

a 的两次调用访问同一个 n,所以第二次得到 12。另一次调用 counter(10) 建立了另一份 n,b 第一次仍得到 11。外层函数返回不会让被闭包引用的变量立即失效;规范要求它们在仍可访问时保持存活。这不等于“每个闭包都必须堆分配”,存储位置是编译器实现问题。[6]

闭包通常读的是变量,不是创建那一刻的值快照。 如果外层后来改变变量,闭包再运行时就可能看到变化。要固定某个值,可以把它作为普通参数传入另一个调用,让新参数保存那一刻的值;但若这个值本身是指针或 slice,复制它仍不会自动深拷贝指向的数据。[6:1][5:1]

这还意味着一个计数闭包本身并不自动具备并发安全性。两个 goroutine 没有同步地读写同一个 n,会形成数据竞争。需要共享计数时,应使用互斥或合适的原子操作;这里只演示串行调用。[7]

循环里要看变量在哪里声明 ​

Go 1.22 起,使用新语言版本语义时,for 语句自己声明的循环变量在各次迭代中独立。复用循环外已经声明的变量,仍然是另一回事:[8]

go
var fresh, shared []func() int
for i := 0; i < 3; i++ {
    fresh = append(fresh, func() int { return i })
}
var i int
for i = 0; i < 3; i++ {
    shared = append(shared, func() int { return i })
}
fmt.Println(fresh[0](), fresh[1](), fresh[2]())
fmt.Println(shared[0](), shared[1](), shared[2]())
text
0 1 2
3 3 3

第一段的 := 在循环里声明变量;第二段的 = 给外面的同一个 i 反复赋值,循环结束时它是 3。因而不能把旧教程里的“全部输出 3”照搬到所有新代码,也不能把新规则理解为“循环里的所有共享引用都安全了”。变量独立,不代表变量指向的对象也独立。文末模块声明 go 1.27.0,所以第一段采用新语义。[8:1]

可变参数:函数里拿到的是 slice ​

func total(xs ...int) int 允许调用者给零个、一个或多个整数。进入函数后,xs 的类型是 []int;不给参数时它是 nil slice。已有 []int 时,用 xs... 把它交给可变参数。[9]

go
xs := []int{1, 2, 3}
fmt.Println(total(), total(1, 2, 3), total(xs...))
replaceFirst(xs...) // 函数内执行 xs[0] = 99
fmt.Println(xs)
text
0 6 6
[99 2 3]

第二个输出说明,xs... 没有复制一份元素。函数拿到的 slice 与原 slice 共用底层数组,改元素会影响调用者。如果 API 应该保留调用者的数据,应先复制元素,或让函数承诺只读;别把 ... 当作隔离边界。[9:1]

调用形式是否可用需要记住的点
total()可以函数必须能处理零个参数
total(1, 2, 3)可以参数类型都是 int
total(xs...),其中 xs 是 []int可以使用原 slice,不自动复制元素
total(1, xs...)不可以不能把同一个可变参数部分写成单个参数再加展开 slice
fmt.Println(names...),其中 names 是 []string不可以[]string 不是 []any

最后一行容易误判:一个 string 可以赋给 any,不表示整个 []string 可以直接当 []any。需要逐个转换时,就创建 []any 并逐个赋值;只是要把这组字符串打印成一个切片,写 fmt.Println(names) 即可。编译器在文末三个反例中明确拒绝了不支持的写法。[9:2][10]

defer:登记发生在现在,调用发生在函数退出前 ​

defer 最适合把“获得资源”和“安排释放资源”放在相邻位置。例如打开文件成功后再登记关闭,避免后面新增一条提前返回路径时漏掉清理。规则并不复杂,但必须拆成两个时间点:[11]

  1. 执行到 defer f(x) 时,求出并保存函数值 f 及参数 x。
  2. 当前函数准备返回,或因 panic 开始退出时,调用已登记的函数;后登记的先执行。

看这一段:

go
func deferred() (result int) {
    n := 1
    defer fmt.Println("argument:", n)
    defer func() { fmt.Println("closure:", n); result++ }()
    n = 2
    return 10
}
fmt.Println("named result:", deferred())
text
closure: 2
argument: 1
named result: 11

两个 defer 看到不同的 n,并不矛盾。第一个在登记时就把参数 1 保存下来了。第二个登记的是一个闭包,它执行时才从外层变量读取 n,此时值已经变成 2。又因为它最后登记,所以最先执行。[11:1][6:2]

命名结果的变化可以按这条时间线阅读:

正在绘制图示…

可切换适合窗口或原始尺寸
查看图源代码
flowchart TD
    A["进入函数:result = 0,n = 1"] --> B["登记 Println,保存参数 n 的值 1"]
    B --> C["登记闭包,闭包引用 n 和 result"]
    C --> D["n = 2"]
    D --> E["return 10:先把 10 赋给 result"]
    E --> F["后登记的闭包先执行:打印 2,result 变成 11"]
    F --> G["先登记的 Println 执行:打印已保存的 1"]
    G --> H["函数完成返回:调用方得到 11"]
放大图示

原始尺寸下可横向或纵向滚动;按 Escape 关闭。

正在绘制图示…

查看图源代码
flowchart TD
    A["进入函数:result = 0,n = 1"] --> B["登记 Println,保存参数 n 的值 1"]
    B --> C["登记闭包,闭包引用 n 和 result"]
    C --> D["n = 2"]
    D --> E["return 10:先把 10 赋给 result"]
    E --> F["后登记的闭包先执行:打印 2,result 变成 11"]
    F --> G["先登记的 Println 执行:打印已保存的 1"]
    G --> H["函数完成返回:调用方得到 11"]

return 10 并不意味着调用方立刻拿到 10。规范规定,返回表达式先求值并赋给结果参数,然后才运行延迟调用。于是闭包还能修改命名结果变量。对照下面的函数:[2:2][11:2]

go
func unnamed() int {
    n := 10
    defer func() { n++ }()
    return n
}
fmt.Println(unnamed())
text
10

这里 n 是普通局部变量。return n 已经把 10 交给结果位置,之后把局部的 n 改成 11,不会回头修改那个返回值。这个区别也说明:用 defer 改返回错误时,要明确修改的是结果变量,不能只修改一个已经被复制出去的局部变量。[2:3][11:3]

保存的不只是参数,还有函数值 ​

go
f := func() { fmt.Println("function: old") }
defer f()
f = func() { fmt.Println("function: new") }

所在函数退出时实际输出:

text
function: old

defer f() 保存了原来的函数值,后来给 f 赋新值不会改变已登记的调用。还有一个容易漏掉的边界:若保存的是 nil 函数值,panic 发生在真正执行延迟调用时,而非执行 defer 语句时。文末测试验证了这个时间差。[11:4]

defer fmt.Println(time.Since(start)) 一类写法也要按同样规则阅读:作为参数出现的函数调用会当场求值。要在退出时测耗时,就把 time.Since(start) 放进延迟执行的闭包里。这里是在应用语言求值规则,并没有声称测过某种 defer 开销。[11:5]

defer 绑定的是函数,不是循环体 ​

同一个函数里,每次循环登记的 defer 都会等到该函数退出。下面用事件记录代替真实文件,方便看出清理时机:

go
func deferScope() (events []string) {
    for i := 0; i < 3; i++ {
        defer func(n int) { events = append(events, fmt.Sprint("close ", n)) }(i)
        events = append(events, fmt.Sprint("use ", i))
    }
    events = append(events, "loop done")
    return
}

func deferPerItem() (events []string) {
    for i := 0; i < 3; i++ {
        func(n int) {
            defer func() { events = append(events, fmt.Sprint("close ", n)) }()
            events = append(events, fmt.Sprint("use ", n))
        }(i)
    }
    return
}

分别打印这两个函数的返回值,实际输出:

text
function scope: [use 0 use 1 use 2 loop done close 2 close 1 close 0]
per item: [use 0 close 0 use 1 close 1 use 2 close 2]

第一行在循环体里直接登记清理,直到循环全部完成才倒序执行。第二行把每一项工作放进单独调用的函数,单次调用结束就清理。两种完整写法都在文末。批量处理文件或连接时,如果希望每一项处理后尽快释放资源,提取 processOne 辅助函数通常更清楚,也不会把大量资源留到整个外层函数结束。[11:6]

延迟函数的返回值会被丢弃,所以单独写 defer f.Close() 不会替你检查关闭错误。是否需要保留该错误取决于操作契约;写文件时若关闭失败也有意义,可以用闭包检查它,并结合主操作错误一起返回。defer 还不是“进程无论如何终止都执行”:os.Exit 会立即退出而不运行它。[11:7][12]

recover:必须由延迟执行的函数直接调用 ​

recover 的意义要放在 panic 的退出过程里理解。它要在同一个 goroutine 中,由正在执行的延迟函数直接调用,才能停止相应的 panic。恢复后,含有该延迟函数的函数结束并返回给调用者;不会跳回 panic(...) 后面继续执行。[13]

写法能否恢复当前 panic关键区别
defer func() { recover() }()能,满足其他条件时延迟函数自己调用 recover
defer cleanup(),cleanup 内直接调用 recover能,满足其他条件时延迟函数可以是有名字的函数
defer func() { helper() }(),由 helper 调用 recover不能中间多了一层普通调用
defer recover()不能没有在延迟函数体内直接调用
在父 goroutine 的 defer 中捕获子 goroutine 的 panic不能不是同一个 goroutine

先看最容易写错的“间接调用”分支。这里保留最外层兜底,避免示例直接崩溃;完整程序还包含表中另外两个分支:

go
func recoverHelper() any { return recover() }

func indirect() (events []string) {
    defer func() { // 先登记,后执行;这里直接调用 recover
        if p := recover(); p != nil {
            events = append(events, "outer: "+fmt.Sprint(p))
        }
    }()
    defer func() { // 后登记,先执行;这里只调用辅助函数
        events = append(events, "helper: "+fmt.Sprint(recoverHelper()))
    }()
    panic("boom")
}

它对应下面输出的第二行。完整程序通过 recovery(kind) 选择三种写法,实际输出是:

text
direct recovery: [direct: boom]
helper recovery: [helper: <nil> outer: boom]
builtin recovery: [outer: boom]

第二行最有解释力:辅助函数里的 recover() 得到 nil,panic 仍然存在,随后由外层直接调用的 recover() 捕获。不要为了复用,把正确的恢复逻辑再包进一个普通辅助函数。defer 的函数可以叫辅助函数,但 recover() 必须就写在它的函数体里。[13:1]

能恢复,不代表恢复后就能继续提供一个可信结果。对于正常可预期的失败,优先设计错误返回;对于 panic,在明确的任务或请求边界恢复、记录必要信息,并返回明确的失败。具体如何区分和包装错误,见《Go 错误处理》。本文测试的是控制流语义,没有把“捕获到了 panic”当作“状态已经恢复正确”。[14]

依据与验证范围 ​

正文的基础是 Go 语言规范,运行结果用于核对具体示例。当前规范和官方 Go 1.27.2 发行包中的 doc/go_spec.html 已交叉核对。循环规则需要注意项目声明的语言版本。

本次在 2026-10-13 用 Go 1.27.2、Linux/amd64 执行了以下检查:

  • go run .:正文全部示例输出与完整程序一致;正文少数片段省略了完整程序中的输出标签。
  • go test -count=1 -v .:5 项测试通过,覆盖恢复调用位置与就近间接恢复片段、nil 延迟函数的 panic 时机、循环变量与闭包独立状态、可变参数共享。
  • go test -race -count=1 .:通过。这里的测试是串行语义测试,不证明计数闭包可并发调用。
  • go vet .:通过。
  • 3 个 testdata 反例分别编译失败,错误摘要见后面的运行命令。

没有测 defer 或闭包的性能,也没有推断它们必然分配在哪种存储区域。os.Exit 不执行 defer 依据官方文档,本篇没有另启进程演示退出。

完整可复现代码 ​

把下列文件放进同一个目录。testdata 里的代码故意不合法,不会被正常的 go test . 当成业务包编译。

运行:

sh
go version
go run .
go test -count=1 -v .
go test -race -count=1 .
go vet .

# 以下 3 条都应该返回非零退出码
go build ./testdata/compare
go build ./testdata/mixed
go build ./testdata/anyslice

本次三个反例的关键诊断分别为:

text
invalid operation: f == f (func can only be compared to nil)
too many arguments in call to sum
cannot use xs (variable of type []string) as []any value in argument to fmt.Println

go.mod ​

go.mod
module example.com/go-functions

go 1.27.0

main.go ​

代码块 · 133 行 · 点击收起 / 展开
go
package main

import "fmt"

type Transform func(int) int

func applyTwice(n int, f Transform) int { return f(f(n)) }
func divmod(a, b int) (int, int)        { return a / b, a % b }
func counter(start int) func() int {
	n := start
	return func() int { n++; return n }
}
func total(xs ...int) int {
	n := 0
	for _, x := range xs {
		n += x
	}
	return n
}
func replaceFirst(xs ...int) {
	if len(xs) > 0 {
		xs[0] = 99
	}
}
func deferred() (result int) {
	n := 1
	defer fmt.Println("argument:", n)
	defer func() { fmt.Println("closure:", n); result++ }()
	n = 2
	return 10
}
func unnamed() int {
	n := 10
	defer func() { n++ }()
	return n
}
func savedFunction() {
	f := func() { fmt.Println("function: old") }
	defer f()
	f = func() { fmt.Println("function: new") }
	_ = f
}
func loopValues() (fresh, shared []int) {
	var a, b []func() int
	for i := 0; i < 3; i++ {
		a = append(a, func() int { return i })
	}
	var i int
	for i = 0; i < 3; i++ {
		b = append(b, func() int { return i })
	}
	for _, f := range a {
		fresh = append(fresh, f())
	}
	for _, f := range b {
		shared = append(shared, f())
	}
	return
}
func recoverHelper() any { return recover() }
func recoverNamed(events *[]string) {
	*events = append(*events, "named: "+fmt.Sprint(recover()))
}
func indirect() (events []string) {
	defer func() {
		if p := recover(); p != nil {
			events = append(events, "outer: "+fmt.Sprint(p))
		}
	}()
	defer func() {
		events = append(events, "helper: "+fmt.Sprint(recoverHelper()))
	}()
	panic("boom")
}
func recovery(kind string) (events []string) {
	defer func() {
		if p := recover(); p != nil {
			events = append(events, "outer: "+fmt.Sprint(p))
		}
	}()
	switch kind {
	case "named":
		defer recoverNamed(&events)
	case "direct":
		defer func() { events = append(events, "direct: "+fmt.Sprint(recover())) }()
	case "helper":
		defer func() { events = append(events, "helper: "+fmt.Sprint(recoverHelper())) }()
	case "builtin":
		defer recover()
	}
	panic("boom")
}
func deferScope() (events []string) {
	for i := 0; i < 3; i++ {
		defer func(n int) { events = append(events, fmt.Sprint("close ", n)) }(i)
		events = append(events, fmt.Sprint("use ", i))
	}
	events = append(events, "loop done")
	return
}
func deferPerItem() (events []string) {
	for i := 0; i < 3; i++ {
		func(n int) {
			defer func() { events = append(events, fmt.Sprint("close ", n)) }()
			events = append(events, fmt.Sprint("use ", n))
		}(i)
	}
	return
}
func main() {
	fmt.Println("twice:", applyTwice(3, func(n int) int { return n + 2 }))
	q, r := divmod(17, 5)
	fmt.Println("divmod:", q, r)
	var absent Transform
	fmt.Println("nil function:", absent == nil)
	a, b := counter(10), counter(10)
	fmt.Println("counters:", a(), a(), b())
	xs := []int{1, 2, 3}
	fmt.Println("variadic:", total(), total(1, 2, 3), total(xs...))
	replaceFirst(xs...)
	fmt.Println("shared slice:", xs)
	fmt.Println("named result:", deferred())
	fmt.Println("unnamed result:", unnamed())
	savedFunction()
	fresh, shared := loopValues()
	fmt.Println("loop declared:", fresh)
	fmt.Println("loop reused:", shared)
	fmt.Println("direct recovery:", recovery("direct"))
	fmt.Println("helper recovery:", recovery("helper"))
	fmt.Println("builtin recovery:", recovery("builtin"))
	fmt.Println("function scope:", deferScope())
	fmt.Println("per item:", deferPerItem())
}

main_test.go ​

代码块 · 59 行 · 点击收起 / 展开
go
package main

import (
	"reflect"
	"testing"
)

func TestRecovery(t *testing.T) {
	cases := []struct {
		kind string
		want []string
	}{
		{"direct", []string{"direct: boom"}},
		{"named", []string{"named: boom"}},
		{"helper", []string{"helper: <nil>", "outer: boom"}},
		{"builtin", []string{"outer: boom"}},
	}
	for _, tc := range cases {
		if got := recovery(tc.kind); !reflect.DeepEqual(got, tc.want) {
			t.Fatalf("%s: %v", tc.kind, got)
		}
	}
}
func TestIndirectSnippet(t *testing.T) {
	if got := indirect(); !reflect.DeepEqual(got, []string{"helper: <nil>", "outer: boom"}) {
		t.Fatal(got)
	}
}
func TestNilFunctionPanicTiming(t *testing.T) {
	registered := false
	defer func() {
		if recover() == nil || !registered {
			t.Fatal("nil deferred call must panic at execution")
		}
	}()
	var f func()
	defer f()
	registered = true
}
func TestLoopsAndClosures(t *testing.T) {
	a, b := loopValues()
	if !reflect.DeepEqual(a, []int{0, 1, 2}) || !reflect.DeepEqual(b, []int{3, 3, 3}) {
		t.Fatal(a, b)
	}
	c, d := counter(0), counter(0)
	if c() != 1 || c() != 2 || d() != 1 {
		t.Fatal("independent counters")
	}
}
func TestVariadicSharing(t *testing.T) {
	if total() != 0 {
		t.Fatal("zero arguments")
	}
	xs := []int{1, 2}
	replaceFirst(xs...)
	if xs[0] != 99 {
		t.Fatal(xs)
	}
}

testdata/compare/main.go ​

go
package main

func main() { f := func() {}; _ = f == f }

testdata/mixed/main.go ​

go
package main

func sum(xs ...int) {}
func main()         { xs := []int{2, 3}; sum(1, xs...) }

testdata/anyslice/main.go ​

go
package main

import "fmt"

func main() { xs := []string{"a", "b"}; fmt.Println(xs...) }

  1. 规范:函数类型、函数声明。 ↩︎ ↩︎ ↩︎

  2. 规范:返回语句、声明与作用域。 ↩︎ ↩︎ ↩︎ ↩︎

  3. 规范:算术运算符。 ↩︎

  4. 规范:函数类型、比较运算、调用。 ↩︎

  5. 规范:调用时的参数赋值。 ↩︎ ↩︎

  6. 规范:函数字面量与闭包。 ↩︎ ↩︎ ↩︎

  7. Go 内存模型:数据竞争及同步要求。 ↩︎

  8. 规范:for 语句、Go 1.22 语言变化。 ↩︎ ↩︎

  9. 规范:传递可变参数。 ↩︎ ↩︎ ↩︎

  10. 规范:可赋值性。 ↩︎

  11. 规范:defer 语句。 ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  12. os.Exit 文档。 ↩︎

  13. 规范:panic 和 recover。 ↩︎ ↩︎

  14. Go 官方:Defer, Panic, and Recover,关于包对外返回显式错误的惯例。 ↩︎

保留各书来源与许可 · 示例运行条件以正文为准