云原生系列Go语言篇-上下文
本文来自正在规划的Go语言&云原生自我提升系列,欢迎关注后续文章。
服务端需要一种处理单个请求元数据的方式。这些元数据可以分为两大类别:一种是在正确处理请求时所需的元数据,另一种是关于何时停止处理请求的元数据。例如,HTTP服务器可能希望使用追踪ID来标识一系列通过一组微服务的请求。它还可能希望设置一个计时器,在对其他微服务的请求时间过长时,就结束这些请求。很多语言使用threadlocal变量来存储此类信息,将数据关联到特定的操作系统执行线程。但在Go语言中,这种方式不可行,因为goroutine没有可以用来查找值的唯一标识。更重要的是,线程本地变量不够清晰,值放在一个地方,却在另一个地方弹出。
Go语言通过一种称为context的结构来解决请求元数据的问题。我们来看如何正确使用它。
上下文是什么
与向语言添加新功能不同,上下文(context)只是实现了context
包中的Context
接口的一个实例。我们知道,地道的Go语言鼓励通过函数参数显式传递数据。上下文也是如此。它只是函数中的另一个参数。就像Go语言约定函数的最后一个返回值是error
一样,还有另一个Go语言的约定,即上下文作为函数的第一个参数在程序中显式传递。上下文参数常命名为ctx
:
func logic(ctx context.Context, info string) (string, error) {
// do some interesting stuff here
return "", nil
}
除了定义Context
接口之外,context
包还包含了一些用于创建和封装上下文的工厂函数。在没有现成上下文时,比如在命令行程序的入口,可以使用函数context.Background
创建一个空的初始上下文。它返回一个context.Context
类型的变量。(是的,这是函数调用返回具体类型的常规模式的一个例外。)
空上下文是一个起点;每次向上下文中添加元数据时,你需要使用context
包中的工厂函数来封装现有的上下文:
注:还有另一个函数
context.TODO
,也创建一个空的context.Context
。它用于在开发时临时使用。如果你不确定上下文来自哪里或如何使用它,可以使用context.TODO
在代码中放置一个占位符。生产代码不应包含context.TODO
。
在编写HTTP服务器时,需要使用稍微不同的模式来获取和传递上下文,通过多层中间件将其传递到顶层的http.Handler
。只是,上下文是有了net/http
很久之后才添加到Go API中的。由于兼容性承诺,无法更改http.Handler
接口来添加context.Context
参数。
兼容性承诺允许向现有类型添加新方法,这正是Go团队所做的。http.Request
上有两个与上下文相关的方法:
Context
返回与请求关联的context.Context
。WithContext
接收context.Context
,并返回一个组合了旧请求状态和context.Context
的新http.Request
。
通常模式如下:
func Middleware(handler http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
ctx := req.Context()
// wrap the context with stuff -- you'll see how soon!
req = req.WithContext(ctx)
handler.ServeHTTP(rw, req)
})
}
在我们的中间件中,首先使用Context
方法从请求中提取现有的上下文。(如果你想跳过这部分,在值小节中会讲到如何将值放入上下文中。)在将值放入上下文后,我们使用WithContext
方法基于旧请求和当前已填充的上下文创建新请求。最后,调用handler
,并将新请求和现有的http.ResponseWriter
传递给它。
在实现handler时,使用Context
方法从请求中提取上下文,并以上下文作为第一个参数调用业务逻辑,就像我们之前学习的那样:
func handler(rw http.ResponseWriter, req *http.Request) {
ctx := req.Context()
err := req.ParseForm()
if err != nil {
rw.WriteHeader(http.StatusInternalServerError)
rw.Write([]byte(err.Error()))
return
}
data := req.FormValue("data")
result, err := logic(ctx, data)
if err != nil {
rw.WriteHeader(http.StatusInternalServerError)
rw.Write([]byte(err.Error()))
return
}
rw.Write([]byte(result))
}
在应用程序中从一个HTTP服务向另一个服务发起HTTP调用时,可以使用net/http
包中的NewRequestWithContext
函数构建包含现有上下文信息的请求:
type ServiceCaller struct {
client *http.Client
}
func (sc ServiceCaller) callAnotherService(ctx context.Context, data string)
(string, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet,
"http://example.com?data="+data, nil)
if err != nil {
return "", err
}
resp, err := sc.client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("Unexpected status code %d",
resp.StatusCode)
}
// do the rest of the stuff to process the response
id, err := processResponse(resp.Body)
return id, err
}
这段代码位于第十四章代码仓库中的sample_code/context_patterns目录中。
我们已经学习了如何获取和传递上下文,下面就开始使用它。先从传值开始。
值
默认情况下,更应通过显式参数传递数据。之前提到过,Go 语言鼓励使用显式而非隐式,其中包括显式数据传递。如果一个函数依赖于某些数据,应该清楚数据来自哪里。
然而,有些情况下我们无法显式传递数据。最常见的情况是 HTTP 请求handler及相关中间件。正如我们所见,所有的 HTTP 请求处理程序都有两个参数,一个是请求,另一个是响应。如果你想在中间件中使某个值对处理程序可用,需要将它存储在上下文中。可能包括从 JWT(JSON Web Token)中提取用户信息,或者创建一个在多层中间件和处理程序以及业务逻辑间传递的各请求的 GUID。
有一个用于将值放入上下文的工厂方法,context.WithValue
。它接受三个值:上下文、用于查找值的键,以及值本身。键和值参数的类型声明为类型any
。context.WithValue
函数返回一个上下文,但并不是传入函数的那个上下文。相反,它是一个包含键值对并封装传入的父上下文(context.Context
)的子上下文。
注: 我们将多次看到这种封装模式。上下文被视为不可变实例。每当我们向上下文添加信息时,都是通过将现有的父上下文封装子上下来实现的。这使我们可以使用上下文将信息传递到更深层的代码中。上下文从不用于将信息从深层传递到更高层。
context.Context
上中的 Value
方法会检查上下文或其父上下文中是否存在某个值。该方法接受一个键(key)并返回与该键关联的值。同样,键参数和值结果的声明类型都是any
。如果找不到所提供键的值,则返回nil
。使用逗号ok语法将返回的值断言为正确的类型:
ctx := context.Background()
if myVal, ok := ctx.Value(myKey).(int); !ok {
fmt.Println("no value")
} else {
fmt.Println("value:", myVal)
}
注: 读者如果熟悉数据结构,你可能会认识到在上下文链中搜索存储的值是一种线性搜索。在只有少量值时,这不会对性能产生严重影响,但如果在请求期间将几十个值存储在上下文中,性能将会很差。也就是说,如果你的程序在创建包含几十个值的上下文链,那么可能需要进行一些重构。
上下文中存储的值可以是任意类型,但选择正确的键非常重要。就像map
的键一样,上下文值的键必须是可比较的。不要只使用像"id"
这样的字符串作为键。如果使用字符串或其他预定义或导出类型作为键的类型,不同的包可能会创建相同的键,导致冲突。这会很难调试,比如一个包向上下文中写入数据,覆盖了另一个包写入的数据,或者从上下文中读取由另一个包写入的数据。
有一种地道的模式可以确保键是唯一且可比较的。基于int
创建一个新的、未导出的键类型:
type userKey int
在声明你的未导出键类型之后,可以声明一个未导出的该类型的常量:
const (
_ userKey = iota
key
)
由于类型和该类型化常量均未导出的,来自包外的代码无法向上下文中放入可能引发冲突的数据。如果你的包需要将多个值放入上下文中,使用我们在错误处理中介绍的 iota 模式为每个值定义一个相同类型的不同键。由于我们只关心用常量的值区分多个键的方法,这是一种iota
的完美用法。
接下来,构建一个 API 来将值放入上下文并从上下文中读取值。仅在包外的代码需要读取和写入上下文值时,才将这些函数设为公开。创建带有值的上下文的函数名称应该以 ContextWith
开头。从上下文中返回值的函数名称应该以 FromContext
结尾。以下是从上下文中获取和读取用户的函数实现示例:
func ContextWithUser(ctx context.Context, user string) context.Context {
return context.WithValue(ctx, key, user)
}
func UserFromContext(ctx context.Context) (string, bool) {
user, ok := ctx.Value(key).(string)
return user, ok
}
现在我们已经编写了用户管理代码,来看如何使用它。我们将编写一个中间件,从 cookie 中提取用户ID:
// a real implementation would be signed to make sure
// the user didn't spoof their identity
func extractUser(req *http.Request) (string, error) {
userCookie, err := req.Cookie("identity")
if err != nil {
return "", err
}
return userCookie.Value, nil
}
func Middleware(h http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
user, err := extractUser(req)
if err != nil {
rw.WriteHeader(http.StatusUnauthorized)
rw.Write([]byte("unauthorized"))
return
}
ctx := req.Context()
ctx = ContextWithUser(ctx, user)
req = req.WithContext(ctx)
h.ServeHTTP(rw, req)
})
}
在该中间件中,我们首先获取用户值。接下来,使用 Context
方法从请求中提取上下文,并使用 ContextWithUser
函数创建一个包含用户的新上下文。在包装上下文时,复用 ctx
变量名是惯用方式。然后,我们使用 WithContext
方法从旧请求和新上下文创建一个新请求。最后,我们使用新请求和传入的 http.ResponseWriter
调用处理程序链中的下一个函数。
在大多数情况下,我们希望在请求处理程序中从上下文中提取值,并显式地传递给业务逻辑。Go 函数具有显式参数,不应使用上下文作为绕过 API 传递值的方式:
func (c Controller) DoLogic(rw http.ResponseWriter, req *http.Request) {
ctx := req.Context()
user, ok := identity.UserFromContext(ctx)
if !ok {
rw.WriteHeader(http.StatusInternalServerError)
return
}
data := req.URL.Query().Get("data")
result, err := c.Logic.BusinessLogic(ctx, user, data)
if err != nil {
rw.WriteHeader(http.StatusInternalServerError)
rw.Write([]byte(err.Error()))
return
}
rw.Write([]byte(result))
}
转载自:https://juejin.cn/post/7250318500882972729