Golang中设置和使用Context值的实践指南
Golang中设置和使用Context值的实践指南 你好,
正如你在下面看到的,我正在创建一个上下文,设置一个键/值对(别在意键的类型,我知道不应该那样定义),并将该上下文用作服务器的 BaseContext,这样我就可以在任何需要的地方获取它,目前运行良好。然而,我想知道的是:
- 就 Go 语言而言,我是否违反了任何规则?
- 如果上述答案是 否,那么我是否也可以使用任何东西作为值?例如,一个多维结构体、作为日志记录器的 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
你违反了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 这句话是其语义的重复。我试图指出你上面违反的规则,如果你不理解,请提问;如果你认为你没有违反规则,请解释原因。我是在公交车上评论的,这是事实。但你得到了你想要的答案,对吧?
无论如何,我在这里放几个链接。
context package - context - Go Packages
context 包定义了 Context 类型,它跨 API 边界和进程之间传递截止时间、取消信号以及其他请求范围的值。
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))
})
}
你的方法在技术上是可行的,但需要调整为每个请求创建独立的上下文实例,并考虑使用类型安全的键来避免运行时错误。

