You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Go中为每个请求生成带追踪Token的日志

为每个请求生成唯一日志ID的Go实现方案

你当前的问题在于全局日志实例的前缀是服务启动时生成的固定ID,导致所有请求共用同一个ID。要实现每个请求对应唯一日志ID,核心是利用Go的context传递请求上下文信息,把请求ID绑定到每个请求的生命周期中。

步骤1:重构基础日志初始化

首先修改日志初始化逻辑,去掉固定的ID前缀,因为ID需要每个请求动态生成:

import (
    "log"
    "time"
    "fmt"
    "context"
    "net/http"
    "github.com/matoous/go-nanoid/v2"
    "gopkg.in/natefinch/lumberjack.v2"
)

var baseLogger *log.Logger

func Init() {
    file := &lumberjack.Logger{
        Filename:   "info.log",
        MaxSize:    20, // 单位:MB
        MaxBackups: 5,
        Compress:   true,
    }
    // 仅初始化基础日志实例,不添加请求ID前缀
    baseLogger = log.New(file, "", log.Ldate|log.Ltime)
}

步骤2:定义上下文键与请求ID注入逻辑

创建用于在context中存储请求ID的专属键,避免与其他上下文值冲突:

// 自定义上下文键类型,避免命名冲突
type ctxKeyRequestID struct{}

// 生成请求ID并注入到上下文
func injectRequestID(ctx context.Context) context.Context {
    id, err := gonanoid.New()
    if err != nil {
        // 生成失败时使用时间戳作为 fallback ID
        id = fmt.Sprintf("fallback-%d", time.Now().UnixNano())
    }
    return context.WithValue(ctx, ctxKeyRequestID{}, id)
}

步骤3:封装带请求ID的日志函数

封装日志打印函数,从上下文自动取出请求ID并拼接到日志内容前:

func Info(ctx context.Context, format string, v ...interface{}) {
    reqID, ok := ctx.Value(ctxKeyRequestID{}).(string)
    if !ok {
        reqID = "unknown-request"
    }
    // 将请求ID作为日志前缀输出
    baseLogger.Printf("[%s] %s", reqID, fmt.Sprintf(format, v...))
}

// 同理可以封装Error、Debug等其他日志级别函数
func Error(ctx context.Context, format string, v ...interface{}) {
    reqID, ok := ctx.Value(ctxKeyRequestID{}).(string)
    if !ok {
        reqID = "unknown-request"
    }
    baseLogger.Printf("[%s] ERROR: %s", reqID, fmt.Sprintf(format, v...))
}

步骤4:通过中间件绑定请求ID到每个请求

如果是HTTP服务,使用中间件为每个进入的请求生成并注入ID:

func RequestIDMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // 为当前请求上下文注入唯一ID
        ctx := injectRequestID(r.Context())
        // 将携带ID的上下文传递给后续处理流程
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

步骤5:在业务逻辑中使用日志函数

在请求处理函数中,直接传入当前请求的上下文调用日志函数即可:

func main() {
    Init()
    mux := http.NewServeMux()
    
    mux.HandleFunc("/api/test", func(w http.ResponseWriter, r *http.Request) {
        // 传入请求上下文,自动携带唯一ID
        Info(r.Context(), "接收到请求,路径:%s", r.URL.Path)
        
        // 模拟业务逻辑
        if err := someBusinessLogic(); err != nil {
            Error(r.Context(), "业务逻辑执行失败:%v", err)
            w.WriteHeader(http.StatusInternalServerError)
            w.Write([]byte("服务器内部错误"))
            return
        }
        
        Info(r.Context(), "请求处理完成")
        w.Write([]byte("请求成功"))
    })
    
    // 用中间件包装路由,确保所有请求都能获取到唯一ID
    http.ListenAndServe(":8080", RequestIDMiddleware(mux))
}

func someBusinessLogic() error {
    // 模拟错误
    return fmt.Errorf("数据库查询超时")
}

替代方案:每个请求创建独立日志实例

如果你更倾向于使用带前缀的日志实例,也可以在中间件中为每个请求创建专属的日志实例,再存入上下文:

type ctxKeyLogger struct{}

func RequestIDMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        id, err := gonanoid.New()
        if err != nil {
            id = fmt.Sprintf("fallback-%d", time.Now().UnixNano())
        }
        // 基于基础日志创建带请求ID前缀的实例
        reqLogger := log.New(baseLogger.Writer(), fmt.Sprintf("[%s] ", id), baseLogger.Flags())
        // 将日志实例存入上下文
        ctx := context.WithValue(r.Context(), ctxKeyLogger{}, reqLogger)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

// 封装日志函数,从上下文取出专属实例
func Info(ctx context.Context, format string, v ...interface{}) {
    logger, ok := ctx.Value(ctxKeyLogger{}).(*log.Logger)
    if !ok {
        logger = baseLogger
    }
    logger.Printf(format, v...)
}

这种方式的优势是无需每次手动拼接请求ID,日志实例的前缀已包含ID,直接调用打印即可。

内容的提问来源于stack exchange,提问作者htan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.18 15:45:32