
本文详解 Go 语言中将日志写入文件的两种主流方式:原生 log 包与第三方库 Logrus,并重点指出因 defer 提前关闭文件导致的“bad file descriptor”常见错误及其根本解决方案。
本文详解 go 语言中将日志写入文件的两种主流方式:原生 `log` 包与第三方库 logrus,并重点指出因 `defer` 提前关闭文件导致的“bad file descriptor”常见错误及其根本解决方案。
在 Go 应用开发初期集成结构化、可持久化的日志能力至关重要。虽然 log 标准库轻量易用,而 Logrus(现维护分支为 github.com/sirupsen/logrus)则提供了更丰富的格式化、Hook 和级别控制能力,但二者在文件输出场景下都需特别注意文件句柄生命周期管理——这是初学者最容易踩坑的关键点。
✅ 正确做法:确保文件句柄在日志使用期间持续有效
1. 使用标准 log 包(安全可靠)
以下示例展示了推荐的写法:在 main() 中打开文件,并通过 defer 在程序退出前关闭(而非在 init 中提前关闭):
package main
import (
"log"
"os"
)
func main() {
// ✅ 在 main 中打开文件,生命周期覆盖整个程序运行期
f, err := os.OpenFile("app.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
log.Fatalf("无法创建日志文件: %v", err)
}
defer f.Close() // ? 此处 defer 在 main 结束时触发,日志可正常写入
log.SetOutput(f)
log.Println("应用启动成功")
log.Printf("用户登录: %s", "alice")
}? 注意:os.O_RDWR 对日志写入非必需,建议使用 os.O_WRONLY + os.O_APPEND 提升安全性与语义清晰度;权限 0644 比 0666 更符合生产环境最小权限原则。
2. 使用 Logrus(避免 init + defer 组合陷阱)
原始问题中的错误根源在于:init() 函数执行完毕后,defer f.Close() 立即触发,导致后续 log.Info() 调用时文件已关闭,从而报 bad file descriptor。
✅ 正确方案:将文件初始化逻辑移至 main(),或使用全局变量持有文件句柄:
package main
import (
"os"
"github.com/sirupsen/logrus"
)
var logFile *os.File
func initLogger() error {
var err error
logFile, err = os.OpenFile("app.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
// 设置 Logrus 输出目标为打开的文件
logrus.SetOutput(logFile)
logrus.SetFormatter(&logrus.JSONFormatter{
TimestampFormat: "2006-01-02 15:04:05",
})
logrus.SetLevel(logrus.DebugLevel)
return nil
}
func main() {
if err := initLogger(); err != nil {
panic(err)
}
defer logFile.Close() // ? 延迟关闭,确保全程可用
logrus.WithFields(logrus.Fields{
"service": "auth",
"version": "1.2.0",
}).Info("服务已启动")
}⚠️ 关键注意事项
- ❌ 禁止在 init() 中使用 defer 关闭日志文件:init() 执行完即释放资源,后续日志必然失败;
- ✅ 始终在 main() 或长期存活的 goroutine 中管理日志文件生命周期;
- ?️ 生产环境建议配合 logrotate 或使用支持轮转的库(如 lumberjack):
import "gopkg.in/natefinch/lumberjack.v2" // ... logrus.SetOutput(&lumberjack.Logger{ Filename: "app.log", MaxSize: 100, // MB MaxBackups: 3, MaxAge: 7, // days Compress: true, })
✅ 总结
Go 日志写入文件的核心原则是:输出目标(io.Writer)必须在整个日志生命周期内保持有效。无论是标准库还是 Logrus,只要遵循“打开 → 配置日志器 → 使用 → 关闭”的顺序,并避免在 init 中引入 defer,即可稳定实现文件日志持久化。掌握这一底层机制,能显著提升 Go 应用可观测性建设的健壮性与可维护性。


















