我试图理解Golang 1.7中引入的上下文是如何工作的,以及将其传递给中间件和HandlerFunc
的适当方法是什么。上下文应该在main函数中初始化,然后传递给checkAuth
函数吗?如何传递给Hanlder
和ServeHTTP
函数?我读了Go并发模式和如何使用上下文,但我很难将这些模式适应我的代码。
func checkAuth(authToken string) util.Middleware {
return func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Auth") != authToken {
util.SendError(w, "...", http.StatusForbidden, false)
return
}
h.ServeHTTP(w, r)
})
}
}
// Handler is a struct
type Handler struct {
...
...
}
// ServeHTTP is the handler response to an HTTP request
func (h *HandlerW) ServeHTTP(w http.ResponseWriter, r *http.Request) {
decoder := json.NewDecoder(r.Body)
// decode request / context and get params
var p params
err := decoder.Decode(&p)
if err != nil {
...
return
}
// perform GET request and pass context
...
}
func main() {
router := mux.NewRouter()
// How to pass context to authCheck?
authToken, ok := getAuthToken()
if !ok {
panic("...")
}
authCheck := checkAuth(authToken)
// initialize middleware handlers
h := Handler{
...
}
// chain middleware handlers and pass context
router.Handle("/hello", util.UseMiddleware(authCheck, Handler, ...))
}
如果你看一下Go并发模式博客文章中的第一个例子,你会注意到它们是从Background
上下文"派生"它们的上下文。这与Request
对象上的Context
和WithContext
方法相结合,可以提供所需的内容。
我刚刚弄明白了这一点(这不是我第一次阅读那些文档);当您"派生"一个上下文时,您正在通过一个更改创建另一个上下文。我已经包装了http.Handler
(实际上使用httprouter.Handle
)。Request.Context
很酷的一点是它永远不会返回nil
;如果没有创建其他上下文,则返回背景上下文。
要在处理程序中指定超时(就在"//perform GET request"注释上方),您可以这样做:
ctx, cancel := context.WithTimeout(r.Context(), time.Duration(60*time.Second))
defer cancel()
r = r.WithContext(ctx)
第一行创建了上下文并给了你cancel钩子,你可以推迟它;当这个延迟调用(第2行)被执行时,一旦请求被服务,任何派生上下文(也就是您添加变量的上下文)都将被取消。最后,第3行替换请求,它现在包含了更新后的上下文。
在授权检查器中,一旦确定用户是有效的,就可以在调用ServeHTTP
之前将用户的信息添加到上下文中。上下文的键不能使用内置类型,但是您可以创建一个新类型,它只是内置类型的别名。为键值定义常量也是一个好主意。一个例子:
type ContextKey string
const ContextUserKey ContextKey = "user"
// Then, just above your call to ServeHTTP...
ctx := context.WithValue(r.Context(), ContextUserKey, "theuser")
h.ServeHTTP(w, r.WithContext(ctx))
这将现在两次派生的上下文(现在具有超时和用户ID)传递给处理程序。
最后,最后一块拼图—如何从处理程序中获取用户ID。这是最直接的部分;我们只需对请求的Context
方法返回的值使用Value
方法。类型是interface{}
,因此如果您想将其视为字符串(如本例所示),则需要类型断言。
user := r.Context().Value(ContextUserKey)
doSomethingForThisUser(user.(string))
你不局限于一个方法的一个更改;只要您继续派生相同的上下文,一旦请求得到服务,当最初派生的上下文(在本例中,我们指定了超时的上下文)在延迟的cancel()
调用触发时被取消时,它将全部被清理。
如果有人试图在handler函数中添加上下文值并在中间件中处理它。
net/http
提供了Request.Clone
和Request.WithContext
方法来修改请求上下文,但都返回一个新的请求指针,不能改变原来的*http.Request
如果不这样做,你可以试试这样做:
func someHandler(w http.ResponseWriter, r *http.Request){
ctx := r.Context()
req := r.WithContext(context.WithValue(ctx, "key", "val"))
*r = *req
}
可以通过添加一个以类型安全的方式从上下文中检索值的函数来改进Daniels的解决方案:
type ContextKey string
const ContextUserKey ContextKey = "user"
func UserFromContext(ctx context.Context) string {
return ctx.Value(ContextUserKey).(string)
}
// Then, just above your call to ServeHTTP...
ctx := context.WithValue(r.Context(), userKey, "theuser")
h.ServeHTTP(w, r.WithContext(ctx))
处理程序不需要强制转换类型,甚至不需要知道上下文键。
在我的一个项目中遇到了这个问题。我能够通过使用服务器结构中的BaseContext字段来解决这个问题。使用自定义上下文初始化BaseContext使服务器能够为所有传入请求拥有此自定义上下文。下面是一些示例代码:
import "net/http"
type ServerHandler struct {
}
server := &http.Server{Addr: localhost:9001,
Handler: ServerHandler,
BaseContext: func(_ net.Listener) context.Context { return <custom context> }}
server.ListenAndServe()
通过用您的自定义上下文初始化BaseContext,您可以将您的自定义上下文传递给ServeHTTP方法。所有传入的请求都有自定义上下文。
func (handler ServerHandler) ServeHTTP(response http.ResponseWriter, request *http.Request) {
//Access your context here as request.Context()
}
在这里找到结构体的定义:https://cs.opensource.google/go/go/+/refs/tags/go1.17:src/net/http/server.go;l=2611