在Go语言的项目开发中,error是内置的错误接口类型,几乎所有可能产生异常的逻辑都会返回error对象,将这些error信息规范地输出到日志中,是后续问题排查和线上故障定位的重要依据。规范的error日志记录需要兼顾信息的完整性、格式的统一性和查询的便捷性,避免仅记录简单的错误提示导致排查时无从下手。

Go内置日志库输出error的基础用法
Go标准库的log包提供了基础的日志输出能力,可以直接将error信息打印到标准输出或者指定文件,适合简单的项目场景。基础用法只需要调用log包的相关方法,将error对象作为参数传入即可。
package main
import (
"errors"
"log"
)
func main() {
// 模拟一个返回error的函数调用
err := doSomething()
if err != nil {
// 直接输出error信息
log.Printf("执行doSomething失败,错误信息: %v", err)
}
}
// 模拟业务逻辑函数,返回error
func doSomething() error {
return errors.New("参数校验不通过,用户ID不能为空")
}
如果需要将日志输出到指定文件,可以通过log.SetOutput方法设置输出目标,示例如下:
package main
import (
"errors"
"log"
"os"
)
func main() {
// 创建或打开日志文件
logFile, err := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
log.Fatalf("打开日志文件失败: %v", err)
}
defer logFile.Close()
// 设置日志输出到文件
log.SetOutput(logFile)
// 添加日志前缀和时间格式
log.SetPrefix("ERROR ")
log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
err = doSomething()
if err != nil {
log.Printf("业务执行失败: %v", err)
}
}
func doSomething() error {
return errors.New("数据库连接超时,请检查数据库状态")
}
第三方日志库记录error的优势
标准库log包功能较为基础,不支持日志级别划分、结构化输出、上下文自动补充等能力,在复杂项目中通常使用第三方日志库,比如zap、logrus等,这些库可以更高效地记录error信息,同时支持更多定制化配置。
使用zap记录error的实践
zap是Uber开源的高性能日志库,支持结构化日志和多种日志级别,非常适合生产环境使用。记录error信息时,可以自动将error对象作为独立字段输出,方便后续日志检索和分析。
package main
import (
"errors"
"go.uber.org/zap"
)
func main() {
// 初始化生产环境logger,默认开启error级别及以上日志输出
logger, err := zap.NewProduction()
if err != nil {
panic(err)
}
defer logger.Sync()
err = queryUserData(1001)
if err != nil {
// 使用Error方法记录error级别日志,自动携带error字段
logger.Error("查询用户信息失败", zap.Error(err), zap.Int("user_id", 1001))
}
}
func queryUserData(userID int) error {
return errors.New("用户不存在,ID为" + string(rune(userID)))
}
使用logrus记录error的实践
logrus是结构化的日志库,API设计更贴近标准库log的使用习惯,上手成本更低,同样支持error信息的结构化记录。
package main
import (
"errors"
"github.com/sirupsen/logrus"
)
func main() {
// 设置日志格式为JSON格式,方便日志系统解析
logrus.SetFormatter(&logrus.JSONFormatter{})
// 设置日志级别为Info,error级别日志会被正常输出
logrus.SetLevel(logrus.InfoLevel)
err := updateOrderStatus(2024)
if err != nil {
logrus.WithError(err).WithField("order_id", 2024).Error("更新订单状态失败")
}
}
func updateOrderStatus(orderID int) error {
return errors.New("订单状态更新权限不足,当前用户无操作权限")
}
Error记录的最佳实践原则
无论使用哪种日志库,记录error信息都需要遵循以下核心原则,才能保证日志的可用性和排查效率:
- 信息完整性:error日志不能只包含错误提示字符串,需要补充错误发生的上下文信息,比如相关参数、操作ID、调用链路标识等,避免仅看到错误提示却不知道对应的业务场景。
- 级别正确性:error级别的日志仅用于记录需要人工介入处理的异常,普通的业务逻辑错误(比如参数校验失败)如果不需要告警,可以使用warn或者info级别,避免error日志泛滥导致重要错误被淹没。
- 格式统一性:整个项目的error日志格式需要统一,优先使用结构化格式(如JSON),方便后续的日志检索、聚合和分析,避免使用杂乱的非结构化文本。
- 避免敏感信息:记录error日志时不要输出用户密码、身份证号、手机号等敏感信息,防止敏感数据泄露。
- 错误包装:在调用链上层记录error时,可以使用
fmt.Errorf配合%w包装下层返回的error,既保留原始错误信息,又补充上层的业务上下文,示例如下:
package main
import (
"errors"
"fmt"
"log"
)
func main() {
err := handleUserRequest(1001)
if err != nil {
log.Printf("处理用户请求失败: %v", err)
// 可以获取原始error信息
var originalErr error
if errors.As(err, &originalErr) {
log.Printf("原始错误原因: %v", originalErr)
}
}
}
func handleUserRequest(userID int) error {
err := queryUserFromDB(userID)
if err != nil {
// 包装error,补充上层业务信息
return fmt.Errorf("处理用户ID为%d的请求失败: %w", userID, err)
}
return nil
}
func queryUserFromDB(userID int) error {
return errors.New("数据库连接池已满,无法建立新的连接")
}
不同场景下的error记录方案
不同的业务场景对error日志的需求不同,需要针对性调整记录策略:
| 场景类型 | 记录策略 |
|---|---|
| 接口入口层 | 记录请求的完整参数、请求ID、返回的error信息,方便定位是哪个请求触发的错误 |
| 底层工具函数 | 不需要直接记录error,仅返回error给上层,由上层调用方决定是否记录日志,避免重复记录 |
| 定时任务场景 | 记录任务执行时间、任务ID、error信息,同时可以配合告警机制,任务失败立即通知负责人 |
| 分布式调用场景 | 携带全局链路追踪ID,将ID记录到error日志中,方便跨服务排查同一请求的错误链路 |
注意:如果error是预期内的业务错误(比如用户重复提交订单),不需要记录为error级别,避免不必要的告警干扰。