
本文介绍如何在 go 中准确、线程安全地计算目录总大小,重点解决全局变量引发的竞态风险,并提供基于闭包的优雅实现方案。
本文介绍如何在 go 中准确、线程安全地计算目录总大小,重点解决全局变量引发的竞态风险,并提供基于闭包的优雅实现方案。
在 Go 中遍历目录并累加文件大小看似简单,但若处理不当,极易引入隐蔽的并发安全问题。原始代码中使用全局变量 dirSize 存储累加结果,虽能短暂工作,却存在两大严重缺陷:一是违反封装原则,破坏函数纯度与可测试性;二是存在数据竞争(race condition)——当多个 goroutine 并发调用 DirSizeMB 时,dirSize 的读写将导致不可预测的结果,Go race detector 可轻易捕获该问题。
正确的做法是将状态(即当前累计大小)限定在函数作用域内。最简洁、惯用的方式是利用匿名函数闭包捕获局部变量。以下是推荐的实现:
import (
"io/fs"
"path/filepath"
)
// DirSize 返回指定路径下所有非目录文件的总字节数。
// 若遍历过程发生 I/O 错误(如权限不足、路径不存在),则返回错误。
func DirSize(path string) (int64, error) {
var size int64
err := filepath.Walk(path, func(_ string, info fs.FileInfo, err error) error {
if err != nil {
return err // 立即返回 I/O 错误(如无法访问子目录)
}
if !info.IsDir() {
size += info.Size()
}
return nil // 继续遍历
})
return size, err
}
✅ 关键改进说明:
- size 是 DirSize 函数内的局部变量,每次调用都拥有独立生命周期,彻底规避竞态;
- 使用 fs.FileInfo(Go 1.16+ 推荐接口)替代旧式 os.FileInfo,增强兼容性;
- 错误处理更严谨:遇到 filepath.Walk 内部错误(如 PermissionDenied)时立即返回,而非忽略;
- 匿名回调函数通过闭包自然捕获 size,语义清晰,无需额外参数或结构体。
若需返回 MB 单位并支持四舍五入,可在此基础上封装(注意:单位转换应在调用层完成,保持核心函数职责单一):
import "math"
// Round 将 f 四舍五入到 prec 精度(如 0.01 表示保留两位小数)
func Round(f float64, prec float64) float64 {
return math.Round(f/prec) * prec
}
// DirSizeMB 返回目录大小(单位:MB),保留两位小数
func DirSizeMB(path string) (float64, error) {
bytes, err := DirSize(path)
if err != nil {
return 0, err
}
mb := float64(bytes) / 1024.0 / 1024.0
return Round(mb, 0.01), nil
}
⚠️ 注意事项:
- filepath.Walk 不保证遍历顺序,但对求和无影响;
- 符号链接默认被跟随(followed),如需跳过符号链接,应改用 filepath.WalkDir 配合 fs.SkipDir 或自定义逻辑;
- 对于超大目录,建议增加上下文(context.Context)支持超时与取消,避免长期阻塞;
- 生产环境建议添加日志或进度反馈机制,便于调试与监控。
综上,摒弃全局状态、拥抱闭包与局部变量,是编写健壮、可并发、符合 Go 语言哲学的目录大小计算函数的核心准则。











