Go 05:defer 何时关闭任务输入
04 的顺序批量入口统计了直接传入的文本。现在允许 --file path 读取一次任务输入:文件打开成功后,读取失败、统计超限和正常返回分别由谁负责关闭文件?如果把 defer file.Close() 写在批量循环内部,为什么进程最终关闭文件,循环期间仍可能占着大量描述符?
同一累计工程本章保留 --text 与 --batch,新增互斥的 --file。文件只作为一条任务的文本来源;读完和关闭以后,才进入原有的字段、字节和码点统计。它不是长期打开的后台任务,也没有把文件放进并发队列。本章完整工程和证据包可在不携带仓库根目录 examples/ 的站点下载,校验值见SHA256SUMS。
独立复跑步骤列出本章正反例、源码校验及运行命令。
先确定文件属于哪个函数
入口检查三个选项恰好选中一个,然后调用 readInputFile(path)。这个函数完成 os.Open 后立即把得到的 *os.File 交给 readAndClose(input io.ReadCloser):
1 | |
这里需要命名结果 err。return string(content), err 先写入结果参数,接着才执行 deferred 闭包,最后把结果交给调用者。读取成功而关闭失败时,闭包把关闭错误写入 err;读取失败时照样调用 Close,但保留读取错误。这是该 CLI 选择的错误优先级,不是 io.ReadAll 自动处理关闭错误。闭包仍能看见稍后写入的命名结果;若改成 defer input.Close(),Close 虽会执行,其返回错误却被丢弃。文件尚未成功打开的路径没有文件可关闭,直接把 os.Open 的错误交给上层。
run 只在 readInputFile 返回后才调用统计函数。读/关出错时写 read file: ... 到 stderr 并返回应用退出码 2,stdout 保持为空。文件中的文本即使含有 | 也作为一条任务统计,不走 --batch 的分割逻辑;同时传 --file 和 --batch 先被互斥检查拒绝。现在全文件经 io.ReadAll 进入内存,没有大小上限;不得用它直接承接不受信任的大文件。后续输入边界章节才解决这个问题。
在 Linux amd64、go version go1.27.1 linux/amd64、GOTOOLCHAIN=local GOWORK=off GOMAXPROCS=2 CGO_ENABLED=0 下,编译出的 taskstats 接收内容为 Go 写 Go 的临时文件,得到 words=3 bytes=9 runes=7,stderr 空,退出 0;指定不存在的文件,stdout 空,stderr 含 read file: open ... no such file or directory,退出 2;--file 和 --batch 同时传入则 stdout 空、usage 到 stderr、退出 2。临时文件的随机路径、实际调用、原始双流和退出码分别保存在 writing-plans/go-language/evidence/05/file.*、missing.*、conflicting.*。go test -count=1 -v ./... 在根包和 stats 包退出 0;TestReadAndClose 分别注入正常读取、关闭错误、读取与关闭同时错误,检查各路径恰好调用一次 Close。关闭失败由测试替身构造,不是本地文件上偶然观察到的故障。
defer 保存参数,闭包稍后读取变量
两个独立短程序放在同一 module 的 testdata/,不会与 CLI 的 main 一起编译。defer-order.go 将命名结果 result 初值设成 1,注册 defer fmt.Println("argument", result),再注册读写 result 的匿名函数,最后执行 return 2。按 Go 1.27.1 的规范:第一处 defer 在注册时计算并保存实参 1;return 2 先设置命名结果为 2;后注册的闭包先运行,打印 2 并改为 3;之前保存的调用打印 1;调用者最终拿到 3。本次 go run ./testdata/defer-order.go 退出 0,原始 stdout 是:
1 | |
这个例子只使用一个 goroutine,没有异步调度。defer fmt.Println("argument", result) 保存的是当时求出的参数;匿名闭包没有把 result 当成传入的参数,在返回阶段读取的仍是那个命名结果变量。实际输入函数的 defer 闭包也借此读取最新的 err,再决定是否写入关闭错误。语言规范保证求值、逆序与命名结果更新的顺序,终端上的三行是本版工具链的运行结果,两者不要互换成证据。
循环结束不是函数返回
假设在同一个批量处理函数内,循环三次各打开一个文件并 defer file.Close()。每轮结束都不会触发这项 defer;三个文件要等外围函数返回才按逆序关闭。testdata/defer-loop.go 真正创建三份临时文件,以进程 ID 前缀在 Linux /proc/self/fd 中计数:函数内部打开三个之后相对基线为 inside-open=3,函数返回后为 after-open=0(命令退出 0)。这个实现先注册删除文件,再注册关闭文件,所以反序执行时先关后删。它是本次 Linux 进程里只计本实验文件的观察,不代表全进程只有三个文件描述符,也不代表任意平台都能读取 /proc/self/fd。
原型最初计的是全部 FD,实际得到 inside-open=5 after-open=2;这混入读取 /proc 等与实验无关的描述符,原始记录保留在 evidence/05/initial-fd-count.*,没有把 5 和 2 解释成未关闭的实验文件。改用前缀过滤并重跑才得到 3 和 0。需要每轮及时释放的真实批量输入,可以在每次迭代调用拥有自己 defer 的辅助函数,或者在确认处理完成后显式 Close;不要把 defer 放在长循环里并误以为每轮都释放了资源。当前 --batch 用的是字符串,不在循环里打开文件,因此没有沿这个反例去改动旧入口。
练习:把上面 readAndClose 的 defer 闭包换成 defer input.Close(),读取成功、关闭失败的测试会怎样?答案:关闭仍被调用,但返回错误没有写入命名结果;TestReadAndClose/close_error 期待关闭错误,会失败。读取与关闭同时失败时,仍应先回答本 CLI 要保留哪个错误;本章选了读取错误,并用 read_and_close_error 子测试锁定。下一篇检查将批量任务组织为 slice 时,复制 slice 值后谁仍能修改底层数组。
验收边界:本章在本机测试、CLI 正反路径和两个 defer 短程序通过;没有运行 race detector、数据库、HTTP、其他操作系统或发布操作。工具链源码 tag go1.27.1、镜像查询的 SHA 862c888e612ac346c7c4d99c9392bdfd265f33b0 已登记,官方 Gitiles 交叉核验仍未完成。页面检查与独立解包复跑结果见素材 RUN.md 与原始日志,不代替本节的 Go 运行证据。
参考资料
- Go 语言规范:Defer statements、Return statements。本章按 Go 1.27.1 随附
doc/go_spec.html的同名小节核对:前者规定求值、调用逆序及可修改命名结果,后者规定 return 的结果赋值顺序。 - os.Open、os.File.Close、io.ReadAll。本机
go doc的实际结果及证据路径见writing-plans/go-language/research/05.md。






