结构化日志 + 统一错误处理机制处理
在生产环境中,可观测性是系统稳定运行的基础能力。
对应用层来说,可观测性主要体现在两个核心点:
日志:记录发生了什么
错误处理:系统为什么会出问题
如果这两项处理得好,你能:快速定位问题;重建请求链路;分析系统瓶颈;支撑监控、告警、追踪
下面我们用 zap+ Go 的错误包装 来构建一套可观测性基础设施。
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…
人生哲理:日志像你的日记,错误像你的跌倒;记录清楚,你就知道哪条路走得最好、哪里需要改进;写得越认真,未来走得越稳。
如果您喜欢这篇文章,请点赞、推荐+分享给更多朋友,万分感谢!