Golang中设置和使用Context值的实践指南

Golang中设置和使用Context值的实践指南 你好,

正如你在下面看到的,我正在创建一个上下文,设置一个键/值对(别在意键的类型,我知道不应该那样定义),并将该上下文用作服务器的 BaseContext,这样我就可以在任何需要的地方获取它,目前运行良好。然而,我想知道的是:

  1. 就 Go 语言而言,我是否违反了任何规则?
  2. 如果上述答案是 ,那么我是否也可以使用任何东西作为值?例如,一个多维结构体、作为日志记录器的 logrus 等等。

谢谢

ctx := context.Background()
ctx = context.WithValue(ctx, "some-key", "some-value")

srv := &http.Server{
	Addr: address,
	Handler: handler,
	BaseContext: func(listener net.Listener) context.Context {
		return ctx
	},
}

更多关于Golang中设置和使用Context值的实践指南的实战教程也可以访问 https://www.itying.com/category-94-b0.html

8 回复

你违反了context values only for request-scoped规则。

更多关于Golang中设置和使用Context值的实践指南的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


任何附带背景说明和理由的答案都将不胜感激。然而,恕我直言,这就像路过时随口说的一句评论。

如果Go语言为我们提供了一个允许我们设置上下文键及其关联值的功能,那么我相信只要正确使用这些键,就没有任何问题。我在我的第一篇帖子中也指出过,我说的是“别在意键的类型,我知道它不应该那样定义”。

我仍然没有看到这里有任何违反上下文使用规则的地方。

假设你在上下文中使用第三方库的键来传递给处理器。

ctx = context.WithValue(ctx, "some-key", "some-value")

在处理器中,如果我看到这个

ctx.Value(“some-key”)

这会很模糊。我可能会认为这个键来自客户端。即使我不知道这个规则,我也无法确定它是客户端发送的,还是服务器配置。这就是为什么我认为 HTTP 上下文值应该用于请求范围的值。

实际上,我的发言根本谈不上“粗鲁”。你只是完全误解了。

这里的开发者水平差异很大。例如,对于初学者来说,你“直接”的答案很容易被认为是“不清晰”的。因此,最好能展开解释一下答案并稍作说明。这会很有帮助。你可能是在“公交车上”匆忙发出了第一条消息,但我认为这既不是我的错,也不是一个合理的理由。

无论如何,我们不要再纠结于此,保持冷静。你何不重新开始,为我解释一下:为什么在“中间件”中设置上下文键/值对是有效的,而像我的例子中那样做就是违规的?我之所以这样问,是因为我见过很多例子(例如)和建议,都是在父上下文中设置上下文键/值对(即使应用程序是通过服务器设置启动的),以便将其传递下去,使其在整个应用程序的“请求范围”内可用。

谢谢

这并非一条万无一失的规则,用“违反”这个词可能不太准确,但在我心中很明确:不应在上下文中使用依赖变量。在你的示例中,我会直接将密钥注入到服务/库/依赖中。

比较以下两种方式:

stripe.Charge(context.Value("key"), email string)

stripe.Charge(server.StripeKey, email string)

第二种方式比第一种清晰得多。它明确表示这是一个注入到服务器中的固定 Stripe 密钥。 第一种方式只说明它在上下文中,但除此之外没有更多信息。它是由中间件传递的吗?是由服务器基础上下文提供的吗?是来自 API 网关吗?还是来自请求本身?

如果你使用多个 Stripe 密钥,并需要根据用户/请求来决定使用哪一个,那么在上下文中使用密钥是有意义的。你可能会有一个中间件,它会根据请求/客户端找出需要使用的 Stripe 密钥,然后你可以通过上下文值来传递这个密钥。正如你所见,这是请求作用域内的。

那么就别这么无礼。

就Go语言而言,我违反了任何规则吗?

我的回答对你的问题来说似乎清晰而直接。不仅如此,context values only for request-scoped 这句话是其语义的重复。我试图指出你上面违反的规则,如果你不理解,请提问;如果你认为你没有违反规则,请解释原因。我是在公交车上评论的,这是事实。但你得到了你想要的答案,对吧?

无论如何,我在这里放几个链接。

favicon

context package - context - Go Packages

context 包定义了 Context 类型,它跨 API 边界和进程之间传递截止时间、取消信号以及其他请求范围的值。

favicon

Context Package Semantics In Go

Ardan Labs 受到小型初创公司和财富 500 强公司的信任,为其培训工程师并开发商业软件解决方案和应用程序。

这是一个很好的实践问题。以下是针对你代码的专业分析:

1. 关于是否违反规则

你的用法没有违反Go语言规则,但存在一个重要的设计问题:你正在使用相同的上下文实例处理所有请求。

// 当前代码的问题:所有请求共享同一个上下文
ctx := context.Background()
ctx = context.WithValue(ctx, "some-key", "some-value")

srv := &http.Server{
    BaseContext: func(listener net.Listener) context.Context {
        return ctx // 所有请求都返回同一个ctx实例
    },
}

这会导致所有HTTP请求共享相同的上下文值,这在并发场景下可能引发问题。正确的做法是为每个请求创建独立的上下文:

type contextKey string

var (
    appConfigKey = contextKey("app-config")
    loggerKey    = contextKey("logger")
)

// 正确的做法:为每个请求创建带值的上下文
srv := &http.Server{
    BaseContext: func(listener net.Listener) context.Context {
        // 为每个监听器创建基础上下文
        baseCtx := context.Background()
        
        // 添加共享的、只读的配置
        config := &AppConfig{
            Version: "1.0.0",
            Env:     "production",
        }
        
        return context.WithValue(baseCtx, appConfigKey, config)
    },
}

// 然后在中间件或处理函数中为每个请求添加请求特定的值
func loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // 为每个请求创建新的上下文
        logger := logrus.WithFields(logrus.Fields{
            "request_id": uuid.New().String(),
            "path":       r.URL.Path,
        })
        
        ctx := context.WithValue(r.Context(), loggerKey, logger)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

2. 关于可以存储的值类型

是的,你可以存储任何类型的值,包括复杂结构体、接口实现等。以下是示例:

// 定义上下文键类型(避免字符串键冲突)
type contextKey string

var (
    dbKey      = contextKey("database")
    cacheKey   = contextKey("cache")
    userKey    = contextKey("user")
    metricsKey = contextKey("metrics")
)

// 复杂结构体示例
type AppDependencies struct {
    DB      *sql.DB
    Cache   *redis.Client
    Logger  *logrus.Logger
    Config  *viper.Viper
}

// 接口实现示例
type MetricsCollector interface {
    IncrementCounter(name string, tags map[string]string)
    RecordTiming(name string, duration time.Duration, tags map[string]string)
}

// 设置多维结构体
func setupContext() context.Context {
    ctx := context.Background()
    
    // 存储复杂结构体
    deps := &AppDependencies{
        DB:     initializeDB(),
        Cache:  initializeRedis(),
        Logger: initializeLogger(),
        Config: loadConfig(),
    }
    ctx = context.WithValue(ctx, dbKey, deps.DB)
    ctx = context.WithValue(ctx, cacheKey, deps.Cache)
    
    // 存储logrus记录器
    logger := logrus.New()
    logger.SetFormatter(&logrus.JSONFormatter{})
    ctx = context.WithValue(ctx, loggerKey, logger)
    
    // 存储接口实现
    metrics := NewPrometheusCollector()
    ctx = context.WithValue(ctx, metricsKey, metrics)
    
    return ctx
}

// 使用示例
func handleRequest(w http.ResponseWriter, r *http.Request) {
    // 获取logrus记录器
    if logger, ok := r.Context().Value(loggerKey).(*logrus.Logger); ok {
        logger.WithFields(logrus.Fields{
            "method": r.Method,
            "ip":     r.RemoteAddr,
        }).Info("Request received")
    }
    
    // 获取数据库连接
    if db, ok := r.Context().Value(dbKey).(*sql.DB); ok {
        var count int
        err := db.QueryRowContext(r.Context(), "SELECT COUNT(*) FROM users").Scan(&count)
        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }
    }
    
    // 获取metrics收集器
    if metrics, ok := r.Context().Value(metricsKey).(MetricsCollector); ok {
        metrics.IncrementCounter("requests.total", map[string]string{
            "path": r.URL.Path,
        })
    }
}

最佳实践建议

// 1. 使用自定义类型作为键,避免包间冲突
type contextKey string

// 2. 提供类型安全的访问函数
func GetLogger(ctx context.Context) *logrus.Entry {
    if logger, ok := ctx.Value(loggerKey).(*logrus.Entry); ok {
        return logger
    }
    // 返回默认记录器而不是nil
    return logrus.NewEntry(logrus.StandardLogger())
}

func GetDB(ctx context.Context) (*sql.DB, error) {
    if db, ok := ctx.Value(dbKey).(*sql.DB); ok && db != nil {
        return db, nil
    }
    return nil, errors.New("database not found in context")
}

// 3. 在中间件中设置请求级别的值
func authMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        user, err := authenticateUser(r)
        if err != nil {
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return
        }
        
        ctx := context.WithValue(r.Context(), userKey, user)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

你的方法在技术上是可行的,但需要调整为每个请求创建独立的上下文实例,并考虑使用类型安全的键来避免运行时错误。

回到顶部