
本文详解 go 语言中使用 logrus 库将日志持久化到文件的正确方式,重点解决因 defer 提前关闭文件导致的“bad file descriptor”错误,并提供可直接运行的完整示例与最佳实践。
本文详解 go 语言中使用 logrus 库将日志持久化到文件的正确方式,重点解决因 defer 提前关闭文件导致的“bad file descriptor”错误,并提供可直接运行的完整示例与最佳实践。
在 Go 中集成结构化日志是构建生产级应用的重要一环。Logrus 作为最广泛使用的日志库之一,支持 JSON 格式、自定义输出、日志级别控制等特性。但初学者常在配置文件输出时遭遇 write testlogrus.log: bad file descriptor 错误——其根本原因在于 init() 函数中使用 defer f.Close() 导致文件句柄在 init 返回时即被关闭,而后续 main() 中的日志调用试图向已关闭的文件写入,自然失败。
✅ 正确做法:避免在 init 中关闭文件,或改用 main 初始化
推荐将日志配置逻辑移至 main() 函数中,并显式管理文件生命周期;若需全局初始化,应将 *os.File 变量提升为包级变量并延迟关闭(例如在 main 结束时用 defer 关闭)。
以下是经过验证的、可直接运行的完整示例:
package main
import (
"os"
"log"
"github.com/sirupsen/logrus" // 注意:新版导入路径为 github.com/sirupsen/logrus(非 Sirupsen,大小写敏感)
)
var logFile *os.File
func initLogger() error {
var err error
// 打开日志文件(追加模式,自动创建)
logFile, err = os.OpenFile("app.log", os.O_CREATE|os.O_APPEND|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 {
log.Fatalf("初始化日志失败: %v", err)
}
defer logFile.Close() // ✅ 在 main 结束时关闭,确保整个生命周期可用
// 示例日志
logrus.WithFields(logrus.Fields{
"animal": "walrus",
"size": 10,
}).Info("A new walrus appears")
logrus.Warn("This is a warning message")
logrus.Error("An unexpected error occurred")
}
⚠️ 关键注意事项:
- 不要在 init() 中使用 defer f.Close():defer 语句会在 init() 函数返回时立即执行,导致文件提前关闭;
- 检查导入路径:Logrus 原作者迁移后,推荐使用 github.com/sirupsen/logrus(小写 sirupsen),旧路径 Sirupsen 可能引发构建问题或版本不兼容;
- 权限与路径:确保程序对目标目录有写权限;相对路径(如 "app.log")基于当前工作目录,建议使用绝对路径或结合 os.Executable() 动态构造日志路径;
- 并发安全:logrus.SetOutput() 设置的是全局输出,Logrus 本身是并发安全的,但若需多文件/多级别输出,应考虑使用 logrus.AddHook() 或自定义 Writer;
- 优雅退出时刷新缓冲:os.File 默认带缓冲,若程序异常终止可能导致日志丢失;生产环境建议配合 logrus.Exit() 或在 defer 前调用 logFile.Sync() 确保落盘(尤其在信号捕获场景下)。
通过以上配置,你将获得结构清晰、时间戳准确、可被 ELK 或 Loki 等日志系统高效采集的 JSON 日志文件。记住:日志是系统的“黑匣子”,正确的初始化方式,是可观测性的第一道防线。











