如何将 Go 中间件模式与错误返回请求处理程序结合起来?
How can I combine Go middleware pattern with error returning request handlers?
我熟悉这样的 Go 中间件模式:
// Pattern for writing HTTP middleware.
func middlewareHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Our middleware logic goes here before executing application handler.
next.ServeHTTP(w, r)
// Our middleware logic goes here after executing application handler.
})
}
例如,如果我有一个 loggingHandler:
func loggingHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Before executing the handler.
start := time.Now()
log.Printf("Strated %s %s", r.Method, r.URL.Path)
next.ServeHTTP(w, r)
// After executing the handler.
log.Printf("Completed %s in %v", r.URL.Path, time.Since(start))
})
}
还有一个简单的 handleFunc:
func handleFunc(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`Hello World!`))
}
我可以这样组合它们:
http.Handle("/", loggingHandler(http.HandlerFunc(handleFunc)))
log.Fatal(http.ListenAndServe(":8080", nil))
没关系。
但我喜欢 Handlers 能够像普通函数一样处理 return 错误的想法。这使得错误处理变得更加容易,因为如果出现错误,我可以 return 一个错误,或者在函数末尾 return nil。
我是这样做的:
type errorHandler func(http.ResponseWriter, *http.Request) error
func (f errorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
err := f(w, r)
if err != nil {
// log.Println(err)
fmt.Println(err)
os.Exit(1)
}
}
func errorHandle(w http.ResponseWriter, r *http.Request) error {
w.Write([]byte(`Hello World from errorHandle!`))
return nil
}
然后像这样包裹起来使用:
http.Handle("/", errorHandler(errorHandle))
我可以让这两种模式分开工作,但我不知道如何将它们结合起来。我喜欢我能够将中间件与像 Alice 这样的库链接在一起。但如果他们也能出现 return 错误就好了。我有办法实现这个目标吗?
根据定义,中间件的输出是 HTTP 响应。如果发生错误,它要么阻止请求得到满足,在这种情况下,中间件应该 return 一个 HTTP 错误(如果服务器上出现意外错误,则为 500),或者它不会,在这种情况下,无论发生什么应该记录下来,以便系统管理员可以修复它,并且应该继续执行。
如果你想通过让你的函数崩溃(虽然我不建议故意这样做)来实现这一点,捕获这种情况并在以后处理它而不会使服务器崩溃,this blog post 中有一个示例在 Panic Recovery 部分(它甚至使用 Alice)。
据我了解,您想链接 errorHandler
函数并将它们组合到 loggingHandler
.
一种方法是使用 struct
将其作为参数传递给您的 loggingHandler
,如下所示:
func loggingHandler(errorHandler ErrorHandler, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Call your error handler to do thing
err := errorHandler.ServeHTTP()
if err != nil {
log.Panic(err)
}
// next you can do what you want if error is nil.
log.Printf("Strated %s %s", r.Method, r.URL.Path)
next.ServeHTTP(w, r)
// After executing the handler.
log.Printf("Completed %s in %v", r.URL.Path, time.Since(start))
})
}
// create the struct that has error handler
type ErrorHandler struct {
}
// I return nil for the sake of example.
func (e ErrorHandler) ServeHTTP() error {
return nil
}
在 main
中你这样称呼它:
func main() {
port := "8080"
// you can pass any field to the struct. right now it is empty.
errorHandler := ErrorHandler{}
// and pass the struct to your loggingHandler.
http.Handle("/", loggingHandler(errorHandler, http.HandlerFunc(index)))
log.Println("App started on port = ", port)
err := http.ListenAndServe(":"+port, nil)
if err != nil {
log.Panic("App Failed to start on = ", port, " Error : ", err.Error())
}
}
我也喜欢这种 HandlerFuncs 返回错误的模式,它更简洁,您只需编写一次错误处理程序。只需将您的中间件与其包含的处理程序分开考虑,您不需要中间件来传递错误。中间件就像一个依次执行每个中间件的链条,最后一个中间件是知道您的处理程序签名并适当处理错误的中间件。
所以在最简单的形式中,保持你拥有的中间件完全相同,但在最后插入一个具有这种形式的中间件(并且不执行另一个中间件,而是执行一个特殊的 HandlerFunc):
// Use this special type for your handler funcs
type MyHandlerFunc func(w http.ResponseWriter, r *http.Request) error
// Pattern for endpoint on middleware chain, not takes a diff signature.
func errorHandler(h MyHandlerFunc) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Execute the final handler, and deal with errors
err := h(w, r)
if err != nil {
// Deal with error here, show user error template, log etc
}
})
}
...
然后像这样包装你的函数:
moreMiddleware(myMiddleWare(errorHandler(myhandleFuncReturningError)))
这意味着这个特殊的错误中间件只能包装您的特殊函数签名,并位于链的末尾,但这没关系。此外,我会考虑将此行为包装在您自己的多路复用器中,以使其更简单一些并避免传递错误处理程序,并让您更轻松地构建中间件链,而无需在路由设置中进行丑陋的包装。
我认为如果您使用路由器库,它可能需要明确支持此模式才能工作。您可以在这个路由器中以修改后的形式看到一个实际的例子,它完全使用您想要的签名,但处理构建中间件链并在没有手动包装的情况下执行它:
最灵活的解决方案是这样的:
首先定义一个与你的处理程序签名相匹配的类型并实现ServeHTTP
以满足http.Handler接口。通过这样做,ServeHTTP
将能够调用处理函数并在失败时处理错误。类似于:
type httpHandlerWithError func(http.ResponseWriter, *http.Request) error
func (fn httpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if err := fn(w, r); err != nil {
http.Error(w, err.Message, err.StatusCode)
}
}
现在照常创建中间件。中间件应该创建一个函数,如果它失败或调用链中的下一个成功则 returns 一个错误。然后将函数转换为定义的类型,例如:
func AuthMiddleware(next http.Handler) http.Handler {
// create handler which returns error
fn := func(w http.ResponseWriter, r *http.Request) error {
//a custom error value
unauthorizedError := &httpError{Code: http.StatusUnauthorized, Message: http.StatusText(http.StatusUnauthorized)}
auth := r.Header.Get("authorization")
creds := credentialsFromHeader(auth)
if creds != nil {
return unauthorizedError
}
user, err := db.ReadUser(creds.username)
if err != nil {
return &httpError{Code: http.StatusInternalServerError, Message: http.StatusText(http.StatusInternalServerError)}
}
err = checkPassword(creds.password+user.Salt, user.Hash)
if err != nil {
return unauthorizedError
}
ctx := r.Context()
userCtx := UserToCtx(ctx, user)
// we got here so there was no error
next.ServeHTTP(w, r.WithContext(userCtx))
return nil
}
// convert function
return httpHandlerWithError(fn)
}
现在您可以像使用任何常规中间件一样使用中间件。
我熟悉这样的 Go 中间件模式:
// Pattern for writing HTTP middleware.
func middlewareHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Our middleware logic goes here before executing application handler.
next.ServeHTTP(w, r)
// Our middleware logic goes here after executing application handler.
})
}
例如,如果我有一个 loggingHandler:
func loggingHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Before executing the handler.
start := time.Now()
log.Printf("Strated %s %s", r.Method, r.URL.Path)
next.ServeHTTP(w, r)
// After executing the handler.
log.Printf("Completed %s in %v", r.URL.Path, time.Since(start))
})
}
还有一个简单的 handleFunc:
func handleFunc(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`Hello World!`))
}
我可以这样组合它们:
http.Handle("/", loggingHandler(http.HandlerFunc(handleFunc)))
log.Fatal(http.ListenAndServe(":8080", nil))
没关系。
但我喜欢 Handlers 能够像普通函数一样处理 return 错误的想法。这使得错误处理变得更加容易,因为如果出现错误,我可以 return 一个错误,或者在函数末尾 return nil。
我是这样做的:
type errorHandler func(http.ResponseWriter, *http.Request) error
func (f errorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
err := f(w, r)
if err != nil {
// log.Println(err)
fmt.Println(err)
os.Exit(1)
}
}
func errorHandle(w http.ResponseWriter, r *http.Request) error {
w.Write([]byte(`Hello World from errorHandle!`))
return nil
}
然后像这样包裹起来使用:
http.Handle("/", errorHandler(errorHandle))
我可以让这两种模式分开工作,但我不知道如何将它们结合起来。我喜欢我能够将中间件与像 Alice 这样的库链接在一起。但如果他们也能出现 return 错误就好了。我有办法实现这个目标吗?
根据定义,中间件的输出是 HTTP 响应。如果发生错误,它要么阻止请求得到满足,在这种情况下,中间件应该 return 一个 HTTP 错误(如果服务器上出现意外错误,则为 500),或者它不会,在这种情况下,无论发生什么应该记录下来,以便系统管理员可以修复它,并且应该继续执行。
如果你想通过让你的函数崩溃(虽然我不建议故意这样做)来实现这一点,捕获这种情况并在以后处理它而不会使服务器崩溃,this blog post 中有一个示例在 Panic Recovery 部分(它甚至使用 Alice)。
据我了解,您想链接 errorHandler
函数并将它们组合到 loggingHandler
.
一种方法是使用 struct
将其作为参数传递给您的 loggingHandler
,如下所示:
func loggingHandler(errorHandler ErrorHandler, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Call your error handler to do thing
err := errorHandler.ServeHTTP()
if err != nil {
log.Panic(err)
}
// next you can do what you want if error is nil.
log.Printf("Strated %s %s", r.Method, r.URL.Path)
next.ServeHTTP(w, r)
// After executing the handler.
log.Printf("Completed %s in %v", r.URL.Path, time.Since(start))
})
}
// create the struct that has error handler
type ErrorHandler struct {
}
// I return nil for the sake of example.
func (e ErrorHandler) ServeHTTP() error {
return nil
}
在 main
中你这样称呼它:
func main() {
port := "8080"
// you can pass any field to the struct. right now it is empty.
errorHandler := ErrorHandler{}
// and pass the struct to your loggingHandler.
http.Handle("/", loggingHandler(errorHandler, http.HandlerFunc(index)))
log.Println("App started on port = ", port)
err := http.ListenAndServe(":"+port, nil)
if err != nil {
log.Panic("App Failed to start on = ", port, " Error : ", err.Error())
}
}
我也喜欢这种 HandlerFuncs 返回错误的模式,它更简洁,您只需编写一次错误处理程序。只需将您的中间件与其包含的处理程序分开考虑,您不需要中间件来传递错误。中间件就像一个依次执行每个中间件的链条,最后一个中间件是知道您的处理程序签名并适当处理错误的中间件。
所以在最简单的形式中,保持你拥有的中间件完全相同,但在最后插入一个具有这种形式的中间件(并且不执行另一个中间件,而是执行一个特殊的 HandlerFunc):
// Use this special type for your handler funcs
type MyHandlerFunc func(w http.ResponseWriter, r *http.Request) error
// Pattern for endpoint on middleware chain, not takes a diff signature.
func errorHandler(h MyHandlerFunc) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Execute the final handler, and deal with errors
err := h(w, r)
if err != nil {
// Deal with error here, show user error template, log etc
}
})
}
...
然后像这样包装你的函数:
moreMiddleware(myMiddleWare(errorHandler(myhandleFuncReturningError)))
这意味着这个特殊的错误中间件只能包装您的特殊函数签名,并位于链的末尾,但这没关系。此外,我会考虑将此行为包装在您自己的多路复用器中,以使其更简单一些并避免传递错误处理程序,并让您更轻松地构建中间件链,而无需在路由设置中进行丑陋的包装。
我认为如果您使用路由器库,它可能需要明确支持此模式才能工作。您可以在这个路由器中以修改后的形式看到一个实际的例子,它完全使用您想要的签名,但处理构建中间件链并在没有手动包装的情况下执行它:
最灵活的解决方案是这样的:
首先定义一个与你的处理程序签名相匹配的类型并实现ServeHTTP
以满足http.Handler接口。通过这样做,ServeHTTP
将能够调用处理函数并在失败时处理错误。类似于:
type httpHandlerWithError func(http.ResponseWriter, *http.Request) error
func (fn httpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if err := fn(w, r); err != nil {
http.Error(w, err.Message, err.StatusCode)
}
}
现在照常创建中间件。中间件应该创建一个函数,如果它失败或调用链中的下一个成功则 returns 一个错误。然后将函数转换为定义的类型,例如:
func AuthMiddleware(next http.Handler) http.Handler {
// create handler which returns error
fn := func(w http.ResponseWriter, r *http.Request) error {
//a custom error value
unauthorizedError := &httpError{Code: http.StatusUnauthorized, Message: http.StatusText(http.StatusUnauthorized)}
auth := r.Header.Get("authorization")
creds := credentialsFromHeader(auth)
if creds != nil {
return unauthorizedError
}
user, err := db.ReadUser(creds.username)
if err != nil {
return &httpError{Code: http.StatusInternalServerError, Message: http.StatusText(http.StatusInternalServerError)}
}
err = checkPassword(creds.password+user.Salt, user.Hash)
if err != nil {
return unauthorizedError
}
ctx := r.Context()
userCtx := UserToCtx(ctx, user)
// we got here so there was no error
next.ServeHTTP(w, r.WithContext(userCtx))
return nil
}
// convert function
return httpHandlerWithError(fn)
}
现在您可以像使用任何常规中间件一样使用中间件。