基于Golang的六边形架构应用实现与日志中间件集成

基于Golang的六边形架构应用实现与日志中间件集成 我正在使用六边形架构开发一个Go语言应用程序。我需要在任何想要使用日志的地方,在日志中打印请求ID。

为了实现这一点,我生成了一个随机的请求ID,并将其附加到GIN上下文中。假设我想在仓库适配器处记录信息。我的问题是,在这种情况下,为了能够记录日志,我需要将上下文对象传递给每一个组件(层)、适配器、端口、领域、仓库以及我想要记录请求ID的每一个方法。这不符合六边形架构和整洁代码的原则。因此,我不想为了获取设置在GIN上下文中的信息,而将上下文传递给每个组件和方法。有没有一种方法,就像我们在Spring中做的那样,一旦主体对象被设置在Spring安全上下文中,我们就可以在任何地方获取到它。


更多关于基于Golang的六边形架构应用实现与日志中间件集成的实战教程也可以访问 https://www.itying.com/category-94-b0.html

4 回复

嗯,我从未使用过 Gin(一般来说,我倾向于远离框架而选择工具包),所以我所说的可能并不正确,但上下文的主要目的不就是在调用栈的每一层进行丰富吗?

正如我所说,我使用 go-kit,因此我有更大的自由度,可以不将非业务相关的信息放入上下文中,但如果你需要这样做,我认为这并没有什么问题。

更多关于基于Golang的六边形架构应用实现与日志中间件集成的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


我也在使用日志中间件,将请求ID和租户ID(因为是多租户应用)设置到Gin上下文中。但我不想为了记录设置在上下文中的详细信息,而承担将上下文对象与其他业务参数一起传递给不同组件的不同函数的负担。我希望这个上下文对象能注入到我想要进行日志记录的函数中。在每个函数中传递上下文对象并与业务参数混合,看起来非常丑陋且代码冗余。

两点:

  1. 在Go语言中,将上下文从传输层(六边形架构中的适配器)向下传递到端口层,再到业务规则层,这种做法是完全可行的。
  2. 对于日志记录,我倾向于构建中间件并将其应用于所有层(适配器日志中间件、端口日志中间件、业务逻辑日志中间件等)。

一个广泛使用日志记录中间件的例子是go-kit的设计方式;可以参考其示例来理解其理念;同样的原则也适用于其他框架。

在六边形架构中避免上下文传递的常见做法是使用上下文传播机制。以下是几种Go语言中的实现方案:

方案一:使用context.Context传递请求ID

// 中间件设置请求ID
func RequestIDMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        requestID := generateRequestID()
        c.Set("X-Request-ID", requestID)
        
        // 创建带请求ID的context
        ctx := context.WithValue(c.Request.Context(), "requestID", requestID)
        c.Request = c.Request.WithContext(ctx)
        c.Next()
    }
}

// 领域层使用(不依赖Gin)
type UserService struct {
    repo UserRepository
}

func (s *UserService) GetUser(ctx context.Context, userID string) (*User, error) {
    // 从context获取请求ID
    if requestID, ok := ctx.Value("requestID").(string); ok {
        log.Printf("[%s] Getting user %s", requestID, userID)
    }
    return s.repo.FindByID(ctx, userID)
}

// 仓库实现
type UserRepositoryImpl struct {
    db *sql.DB
}

func (r *UserRepositoryImpl) FindByID(ctx context.Context, userID string) (*User, error) {
    // 从context获取请求ID用于日志
    if requestID, ok := ctx.Value("requestID").(string); ok {
        log.Printf("[%s] Querying database for user %s", requestID, userID)
    }
    // 数据库操作...
}

方案二:使用goroutine本地存储(类似ThreadLocal)

// 定义请求上下文键类型
type contextKey string

const requestIDKey contextKey = "requestID"

// 上下文管理器
type RequestContext struct{}

func (rc *RequestContext) SetRequestID(ctx context.Context, requestID string) context.Context {
    return context.WithValue(ctx, requestIDKey, requestID)
}

func (rc *RequestContext) GetRequestID(ctx context.Context) string {
    if id, ok := ctx.Value(requestIDKey).(string); ok {
        return id
    }
    return ""
}

// 日志包装器
type Logger struct {
    context *RequestContext
}

func (l *Logger) Info(ctx context.Context, msg string, fields ...interface{}) {
    requestID := l.context.GetRequestID(ctx)
    log.Printf("[%s] %s", requestID, fmt.Sprintf(msg, fields...))
}

// 使用示例
func main() {
    router := gin.Default()
    ctxManager := &RequestContext{}
    logger := &Logger{context: ctxManager}
    
    router.Use(func(c *gin.Context) {
        requestID := generateRequestID()
        ctx := ctxManager.SetRequestID(c.Request.Context(), requestID)
        c.Request = c.Request.WithContext(ctx)
        c.Next()
    })
    
    router.GET("/users/:id", func(c *gin.Context) {
        userID := c.Param("id")
        svc := NewUserService(ctxManager, logger)
        
        // 传递context到领域层
        user, err := svc.GetUser(c.Request.Context(), userID)
        if err != nil {
            c.JSON(500, gin.H{"error": err.Error()})
            return
        }
        c.JSON(200, user)
    })
}

方案三:结构化日志与依赖注入

// 日志接口定义在领域层
package domain

type Logger interface {
    Info(msg string, fields ...interface{})
    WithRequestID(requestID string) Logger
}

// 适配器层实现
package infrastructure

type StructuredLogger struct {
    requestID string
    logger    *log.Logger
}

func (l *StructuredLogger) Info(msg string, fields ...interface{}) {
    entry := fmt.Sprintf("[%s] %s", l.requestID, msg)
    l.logger.Printf(entry, fields...)
}

func (l *StructuredLogger) WithRequestID(requestID string) Logger {
    return &StructuredLogger{
        requestID: requestID,
        logger:    l.logger,
    }
}

// 应用服务使用
package application

type UserService struct {
    repo   domain.UserRepository
    logger domain.Logger
}

func NewUserService(repo domain.UserRepository, logger domain.Logger) *UserService {
    return &UserService{
        repo:   repo,
        logger: logger,
    }
}

func (s *UserService) GetUser(userID string) (*domain.User, error) {
    s.logger.Info("Getting user", "userID", userID)
    return s.repo.FindByID(userID)
}

// HTTP处理器
package adapter

type GinHandler struct {
    userService *application.UserService
    logger      domain.Logger
}

func (h *GinHandler) GetUser(c *gin.Context) {
    requestID := c.GetString("X-Request-ID")
    userID := c.Param("id")
    
    // 创建带请求ID的logger实例
    requestLogger := h.logger.WithRequestID(requestID)
    
    // 注入到服务(可通过工厂或依赖注入容器)
    userService := application.NewUserService(
        h.userService.repo,
        requestLogger,
    )
    
    user, err := userService.GetUser(userID)
    if err != nil {
        c.JSON(500, gin.H{"error": err.Error()})
        return
    }
    c.JSON(200, user)
}

方案四:使用OpenTelemetry进行分布式追踪

import (
    "context"
    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/trace"
)

// 中间件设置追踪
func TracingMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        tracer := otel.Tracer("gin-server")
        ctx, span := tracer.Start(c.Request.Context(), c.Request.URL.Path)
        defer span.End()
        
        // 从span获取TraceID作为请求ID
        traceID := span.SpanContext().TraceID().String()
        ctx = context.WithValue(ctx, "requestID", traceID)
        
        c.Request = c.Request.WithContext(ctx)
        c.Next()
    }
}

// 在任何地方获取追踪ID
func logWithTrace(ctx context.Context, msg string) {
    if span := trace.SpanFromContext(ctx); span.IsRecording() {
        traceID := span.SpanContext().TraceID().String()
        log.Printf("[%s] %s", traceID, msg)
    }
}

推荐使用方案一或方案二,它们保持了六边形架构的边界清晰,同时避免了上下文对象的过度传递。context.Context是Go语言标准库提供的标准解决方案,适合在请求处理链中传递请求范围的值。

回到顶部