filepath.walkdir扫描日志目录时需自定义walkdirfunc处理权限错误、用modtime而非字符串比较判断旧日志、压缩时写临时文件并校验后再重命名、显式调用gzip.writer.close()、删除前确认.gz文件非空且有效。

用 filepath.WalkDir 扫描日志目录时要注意遍历顺序和权限错误
Go 1.16+ 推荐用 filepath.WalkDir 替代旧的 filepath.Walk,它支持按需读取、跳过子树,且能区分 symlink 和真实目录。但默认不处理权限拒绝(如 permission denied 错误),直接中断遍历。
实操建议:
- 传入自定义
fs.WalkDirFunc,在回调中判断err != nil且errors.Is(err, fs.ErrPermission)时返回nil继续遍历(不是filepath.SkipDir) - 避免用
strings.HasSuffix(path, ".log")判断日志文件——有些日志带日期后缀(如app.log.2024-05-01),应优先检查文件名前缀或正则匹配 - 若监控多级日志目录(如
/var/log/nginx/下有access/、error/子目录),注意WalkDir默认深度优先,无需额外排序;但压缩前建议按ModTime()倒序排列,确保旧文件优先处理
用 gzip.Writer 压缩时别直接写原文件,也别忽略 Close()
常见错误是打开原日志文件后,用 os.Create 覆盖同名 .gz 文件,再用 gzip.NewWriter 写入——这会导致压缩流未刷新、文件体积为 0 或损坏。
实操建议:
- 始终先写入临时文件(如
app.log.2024-05-01.gz.tmp),压缩完成并校验(gzip.NewReader尝试解压头)后再os.Rename替换 -
gzip.Writer必须显式调用w.Close(),否则内部缓冲区不 flush,压缩数据丢失;建议用defer w.Close(),但要捕获其返回的 error(可能因 I/O 失败而报错) - 不要用
io.Copy(w, f)后立刻关源文件——io.Copy不保证读完,应确保源文件Read返回io.EOF后再关闭
判断“旧日志”的核心是 time.Time.Before,不是字符串比较
有人用 strings.Contains(name, "2023") 或 name 判断是否该压缩,极易出错:日志轮转格式不统一(<code>app.log.1、app.log.20240501、app.log-20240501),字符串比较会错判或 panic。
实操建议:
- 统一用
fi.ModTime().Before(cutoffTime)判断,cutoffTime = time.Now().AddDate(0, 0, -7)表示 7 天前 - 若必须从文件名提取时间(如 logrotate 生成的
app.log-20240501),用time.Parse("20060102", suffix)解析,而非strconv.Atoi拼年月日——后者无法处理2024-05-01格式 - 对软链接文件,
os.Stat获取的是链接本身时间,应改用os.Lstat+filepath.EvalSymlinks再os.Stat取目标修改时间
用 os.Remove 删除原日志前,必须确认压缩成功且文件非空
脚本里常见 bug 是:压缩函数返回 nil error,但生成的 .gz 文件大小为 0(比如源文件被其他进程锁住、读取中途 EOF),此时删掉原日志就丢数据了。
实操建议:
- 压缩完成后,立即用
os.Stat检查.gz文件:if info.Size() == 0 { return fmt.Errorf("compressed file is empty") } - 加一层校验:用
gzip.NewReader打开刚写的.gz文件,调用reader.Multistream(false)并读取前几个字节(io.Discard即可),验证 gzip header 有效 -
os.Remove原文件前,加上time.Sleep(100 * time.Millisecond)避免 NFS 或某些日志框架(如 systemd-journald)的缓存延迟导致误删
最易被忽略的是信号处理:如果脚本运行中收到 SIGHUP 或 SIGTERM,正在压缩的文件可能残留 .tmp 后缀,下次运行会重复处理。需要注册 signal.Notify,在退出前清理临时文件,且临时文件名应包含 PID 或随机后缀防冲突。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











