每日一Go-22、Go语言实战-应用可观测性:日志与错误处理

0 阅读4分钟

图片

结构化日志 + 统一错误处理机制处理

在生产环境中,可观测性是系统稳定运行的基础能力。

对应用层来说,可观测性主要体现在两个核心点:

日志:记录发生了什么

错误处理:系统为什么会出问题

如果这两项处理得好,你能:快速定位问题;重建请求链路;分析系统瓶颈;支撑监控、告警、追踪

下面我们用 zap+ Go 的错误包装 来构建一套可观测性基础设施。

zap(pkg.go.dev/go.uber.org…

1、为什么一定要用结构化日志?

传统日志:

//nginx
user login success id=1 ip=10.0.0.1

结构化日志:

//json
{
  "time""2025-11-17T10:30:02Z",
  "level""info",
  "msg""user login success",
  "user_id": 1,
  "ip""10.0.0.1"
}

优势:能被 ELK、Loki、Datadog、CloudWatch 等平台直接解析;能按字段搜索(如 user_id);可用于 Dashboard(例如统计失败率);更利于机器处理、聚合分析;因此结构化日志 = 云原生标准。

2、使用zap实现结构化日志

2.1 初始化zap,config/log.go

package config
import (
    "github.com/gin-gonic/gin"
    "go.uber.org/zap"
)
func InitLog() {
    var logger *zap.Logger
    var err error
    if gin.Mode() == gin.DebugMode {
        logger, err = zap.NewDevelopment()
    } else {
        logger, err = zap.NewProduction()
    }
    if err != nil {
        panic("初始化Zap日志失败:" + err.Error())
    }
    zap.ReplaceGlobals(logger)
}

2.2 使用zap,新增中间件middlewares/log.middleware.go

package middlewares
import (
    "math"
    "os"
    "time"
    "github.com/gin-gonic/gin"
    "go.uber.org/zap"
)
func LoggerMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        startTime := time.Now()
        c.Next() // 调用该请求的剩余处理程序
        stopTime := time.Since(startTime)
        spendTime := int(math.Ceil(float64(stopTime.Nanoseconds() / 1000000)))
        hostName, err := os.Hostname()
        if err != nil {
            hostName = "Unknown"
        }
        statusCode := c.Writer.Status()
        clientIP := c.ClientIP()
        userAgent := c.Request.UserAgent()
        dataSize := c.Writer.Size()
        if dataSize < 0 {
            dataSize = 0
        }
        method := c.Request.Method
        url := c.Request.RequestURI
        // 构建 Zap 字段
        fields := []zap.Field{
            zap.String("HostName", hostName),
            zap.Int("SpendTime", spendTime),
            zap.String("path", url),
            zap.String("Method", method),
            zap.Int("status", statusCode),
            zap.String("Ip", clientIP),
            zap.Int("DataSize", dataSize),
            zap.String("UserAgent", userAgent),
        }
        // 添加错误信息(如果有)
        if len(c.Errors) > 0 {
            fields = append(fields, zap.Strings("errors", c.Errors.Errors()))
        }
        // 根据状态码级别记录日志
        switch {
        case statusCode >= 500:
            // L returns the global Logger, which can be reconfigured with ReplaceGlobals. It's safe for concurrent use.
            // zap.L()返回zap.ReplaceGlobals(logger)设置的全局logger,它是并发安全的
            zap.L().Error("HTTP Server Error", fields...)
        case statusCode >= 400:
            zap.L().Warn("HTTP Client Error", fields...)
        default:
            zap.L().Info("HTTP Request", fields...)
        }
    }
}

2.3 添加全局中间件初始化 middlewares/init.go

package middlewares
import (
    "github.com/gin-gonic/gin"
)
func InitMiddlewares(r *gin.Engine) {
    r.Use(LoggerMiddleware())
}

2.4 在初始化路由之前绑定中间件

// 初始化路由
func InitRouter() *gin.Engine {
    r := gin.Default()
    // 新增一行,把中间件绑定到所有路由上
    middlewares.InitMiddlewares(r)
    for _, route := range routers {
        route(r)
    }
    return r
}

3、构建统一的错误处理机制

    在大型项目中,缺少统一错误机制会导致:

  • 错误格式不一致

  • 有些错误没日志

  • 用户能看到内部错误信息

  • 业务错误和系统错误混在一起

  • 不能很好地追踪错误来源

    我们希望实现:

  • 错误包装,保留调用栈信息

  • 区分业务错误和系统错误,便于不同处理逻辑

  • 每个错误都能被上层捕获,并输出结构化日志

  • 控制器返回统一格式,而不是随便返回

3.1 定义业务错误类型 errors/biz.error.go

package errors
// 业务逻辑错误定义
type BizError struct {
    Code int
    Msg  string
}
func (e *BizError) Error() string {
    return e.Msg
}
func NewBizError(code int, msg string) *BizError {
    return &BizError{
        Code: code,
        Msg:  msg,
    }
}

3.2 在服务层日志+错误日志包装(%w)

func (s *UserService) GetById(c *gin.Context) (user models.User, err error) {
    ...
    // 没取到,从repo里取数据
    user, err = repos.User.GetById(uid)
    if err != nil {
        fields := []zap.Field{
            zap.Uint("user_id", uid),
            zap.String("trace_id", c.GetString(`trace_id`)),
        }
        zap.L().Warn("repo find user failed", fields...)
        err = fmt.Errorf("service GetById:%w", err)
        return
    }
    ...
}

3.3 控制层统一返回错误格式

// 查询当前登录者的信息
func Me(c *gin.Context) {
    user, err := services.User.GetById(c)
    if err != nil {
        // 业务错误
        if biz, ok := err.(*errors.BizError); ok {
            c.JSON(http.StatusBadRequest, gin.H{
                "code": biz.Code,
                "msg":  biz.Msg,
            })
            return
        }
        fields := []zap.Field{zap.String("trace_id", c.GetString(`trace_id`))}
        zap.L().Error(err.Error(), fields...)
        c.JSON(http.StatusInternalServerError, gin.H{
            `code`: http.StatusInternalServerError,
            `msg`:  err.Error(),
        })
        return
    }
    c.JSON(http.StatusOK, gin.H{
        "code": http.StatusOK,
        "msg":  "查询成功",
        "user": gin.H{
            "id":       user.ID,
            "username": user.Username,
        },
    })
}

4、Context+日志:将trace_id串起来

//middlewares/trace_id.middleware.go
package middlewares
import (
    "github.com/gin-contrib/requestid"
    "github.com/gin-gonic/gin"
)
func TraceIdMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Set(`trace_id`, requestid.Get(c))
        c.Next()
    }
}
//middlewares/init.go
import (
    "github.com/gin-contrib/requestid"
    "github.com/gin-gonic/gin"
)
func InitMiddlewares(r *gin.Engine) {
    r.Use(requestid.New())
    r.Use(TraceIdMiddleware())
    r.Use(LoggerMiddleware())
}
//middlewares/log.middleware.go
package middlewares
import (
    "math"
    "os"
    "time"
    "github.com/gin-gonic/gin"
    "go.uber.org/zap"
)
func LoggerMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        startTime := time.Now()
        c.Next() // 调用该请求的剩余处理程序
        stopTime := time.Since(startTime)
        spendTime := int(math.Ceil(float64(stopTime.Nanoseconds() / 1000000)))
        hostName, err := os.Hostname()
        if err != nil {
            hostName = "Unknown"
        }
        statusCode := c.Writer.Status()
        clientIP := c.ClientIP()
        userAgent := c.Request.UserAgent()
        dataSize := c.Writer.Size()
        if dataSize < 0 {
            dataSize = 0
        }
        method := c.Request.Method
        url := c.Request.RequestURI
        raw := c.Request.URL.RawQuery
        // 构建 Zap 字段
        fields := []zap.Field{
            zap.String("hostName", hostName),
            zap.Int("spendTime", spendTime),
            zap.String("path", url),
            zap.String("query", raw),
            zap.String("method", method),
            zap.Int("status", statusCode),
            zap.String("clientIp", clientIP),
            zap.Int("dataSize", dataSize),
            zap.String("userAgent", userAgent),
            zap.String("trace_id", c.GetString(`trace_id`)),
        }
        // 添加错误信息(如果有)
        if len(c.Errors) > 0 {
            fields = append(fields, zap.Strings("errors", c.Errors.Errors()))
        }
        // 根据状态码级别记录日志
        switch {
        case statusCode >= 500:
            // L returns the global Logger, which can be reconfigured with ReplaceGlobals. It's safe for concurrent use.
            // zap.L()返回zap.ReplaceGlobals(logger)设置的全局logger,它是并发安全的
            zap.L().Error("HTTP Server Error", fields...)
        case statusCode >= 400:
            zap.L().Warn("HTTP Client Error", fields...)
        default:
            zap.L().Info("HTTP Request", fields...)
        }
    }
}

5、架构整合:一条完整链路的可观测性

[Controller]
  ↓ 记录 trace_id
[Service]
  ↓ 包装(wrap)业务逻辑错误
[Repository]
  ↓ 返回数据库错误
[Service]
  ↓ 写结构化日志 + 添加上下文
[Controller]
  ↓ 统一输出给前端(JSON)

你可以:

  • 一眼看出哪个模块出问题

  • 搜索trace_id看到全链路日志

  • 统一格式便于检索与监控

  • 业务错误和系统错误完全分离

真正实现:可观测性驱动的高稳定性Go服务。

6、源码地址 pan.baidu.com/s/1B6pgLWfS…

人生哲理:日志像你的日记,错误像你的跌倒;记录清楚,你就知道哪条路走得最好、哪里需要改进;写得越认真,未来走得越稳。


如果您喜欢这篇文章,请点赞、推荐+分享给更多朋友,万分感谢!