bufio.Scanner无法替代tail -f,因其仅单次读取遇EOF即终止,不监听文件变化、不处理日志轮转或重建;需用github.com/hpcloud/tail等库或手动轮询实现持续追踪。

直接用 os.Open + bufio.Scanner 读完就停,根本做不到“实时增量显示”——它不监听变化,也不重试 EOF。真要类 tail -f 行为,必须用事件驱动或轮询机制持续检查文件增长,且得处理日志滚动、删除重建等边界情况。
为什么 bufio.Scanner 无法替代 tail -f
常见错误现象:程序启动后打印完当前内容就退出,新写入的日志完全看不到;或者遇到文件被 logrotate 切走后 panic 报 file does not exist。
根本原因在于:Scanner 是单次流式读取器,遇到 EOF 就返回 false,不会等待新数据。它不感知文件 inode 变更、不重 open、不处理 ReOpen 场景。
适用场景:一次性读取静态文件(如配置、JSON 日志快照)
立即学习“go语言免费学习笔记(深入)”;
不适用场景:监控 /var/log/syslog、app.log 等持续追加的运行时日志
用 github.com/hpcloud/tail 实现可靠追踪
这个库已维护多年,行为最贴近原生 tail -f,支持 inotify(Linux)、kqueue(macOS)和 polling(Windows),能自动处理日志滚动。
实操建议:
- 安装:
go get github.com/hpcloud/tail - 从文件末尾开始(避免刷屏旧内容):
config.Location = &tail.SeekInfo{Offset: 0, Whence: os.SEEK_END} - 启用
ReOpen: true应对logrotate删除重建文件的场景 - 设
MustExist: false允许等待文件创建(比如容器启动稍晚于日志收集器) - 别漏
Follow: true,否则只读一次就退出
示例关键片段:
t, err := tail.TailFile("/var/log/app.log", tail.Config{
Follow: true,
ReOpen: true,
MustExist: false,
Location: &tail.SeekInfo{Offset: 0, Whence: os.SEEK_END},
})
if err != nil {
log.Fatal(err)
}
for line := range t.Lines {
fmt.Println(line.Text)
}
手动轮询实现轻量 fallback(无第三方依赖)
当不能引入 cgo 或外部库(如某些嵌入式/安全受限环境),可用 os.Stat + io.Seek 轮询模拟,但需自己兜底所有异常路径。
核心逻辑:
- 首次打开文件,记录初始
Size()和ModTime() - 循环中定期
os.Stat,若Size > lastSize,则file.Seek(lastSize, 0)定位到新起点,io.Copy剩余部分 - 若
Stat报os.IsNotExist,且ReOpen开启,则 sleep 后重试 - 若
ModTime变更但Size不变(如 trunc),需重置 offset 并重新读
注意:轮询间隔别太短(如 100ms),否则空转耗 CPU;也别太长(如 5s),否则延迟明显。推荐 250ms–1s。
容易被忽略的细节:文件句柄泄漏与权限继承
长期运行的 tail 工具若没正确关闭 t,会导致文件句柄泄露——尤其在频繁滚动日志时,旧文件句柄不释放,最终 hit ulimit。
务必在退出前调用 t.Stop(),并在 defer 中确保关闭:
defer func() {
if t != nil {
t.Stop()
t.Cleanup() // 关闭底层文件句柄
}
}()
另一个坑是权限:用 os.OpenFile 打开日志时若未显式传 0644 等 mode,新建临时文件(如 logrotate 创建)可能因 umask 导致不可读。生产环境应统一设置 os.FileMode(0644) 显式控制。


















