了解Go中的http handlerfunc包装器技术

Gho*_*orm 2 rest http wrapper go gorilla

我看到了Mat Ryer撰写一篇文章,内容涉及如何使用服务器类型和HTTP处理程序类型的包装程序,func(http.ResponseWriter, *http.Request)

我认为这是一种构建REST API的更优雅的方式,但是我完全迷住了使包装器正常运行的想法。我要么在编译时收到类型不匹配的错误,要么在调用时收到404的错误。

这基本上是我目前用于学习的目的。

package main

import(
   "log"
   "io/ioutil"
   "encoding/json"
   "os"
   "net/http"
   "github.com/gorilla/mux"
)

type Config struct {
   DebugLevel int `json:"debuglevel"`
   ServerPort string `json:"serverport"`
}

func NewConfig() Config {

   var didJsonLoad bool = true

   jsonFile, err := os.Open("config.json")
   if(err != nil){
      log.Println(err)
      panic(err)
      recover()
      didJsonLoad = false
   }

   defer jsonFile.Close()

   jsonBytes, _ := ioutil.ReadAll(jsonFile)

   config := Config{}

   if(didJsonLoad){
      err = json.Unmarshal(jsonBytes, &config)
      if(err != nil){
         log.Println(err)
         panic(err)
         recover()
      }
   }

   return config
}

type Server struct {
   Router *mux.Router
}

func NewServer(config *Config) *Server {
   server := Server{
      Router : mux.NewRouter(),
   }

   server.Routes()

   return &server
}

func (s *Server) Start(config *Config) {
   log.Println("Server started on port", config.ServerPort)
   http.ListenAndServe(":"+config.ServerPort, s.Router)
}

func (s *Server) Routes(){
   http.Handle("/sayhello", s.HandleSayHello(s.Router))
}

func (s *Server) HandleSayHello(h http.Handler) http.Handler {
   log.Println("before")
   return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request){
      w.Write([]byte("Hello."))
      h.ServeHTTP(w, r)
   })
}

func main() {
   config := NewConfig()
   server := NewServer(&config)
   server.Start(&config)
}
Run Code Online (Sandbox Code Playgroud)

目前,我将只返回404调用localhost:8091/sayhello。(是的,这是我在配置文件中设置的端口。)

以前,由于我使用的是Gorilla Mux,所以我将处理程序设置如下:

func (s *Server) Routes(){
    s.Router.HandleFunc("/sayhello", s.HandleSayHello)
}
Run Code Online (Sandbox Code Playgroud)

这给了我这个错误,我完全被绊倒了。 cannot use s.HandleSayHello (type func(http.Handler) http.Handler) as type func(http.ResponseWriter, *http.Request) in argument to s.Router.HandleFunc

我在此SO帖子的解决方案中看到了我应该使用http.Handle并传递到路由器中的内容。

func (s *Server) Routes(){
   http.Handle("/sayhello", s.HandleSayHello(s.Router))
}
Run Code Online (Sandbox Code Playgroud)

但是现在如何设置路由时阻止实际功能执行?将"before"在我的打印语句显示了服务器开始之前。我现在不认为这是一个问题,但是可能是我开始打算为数据库查询编写更复杂的中间件之后。

进一步研究该技术后,我发现其他读数表明我需要定义一个middlewarehandler类型。

我不完全理解这些示例中发生的事情,因为它们定义的类型似乎没有被使用。

此资源显示如何编写处理程序,但不显示如何设置路由。

我确实发现Gorilla Mux已经为这些东西内置了包装器,但是我很难理解API。

他们显示的示例如下所示:

func loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Do stuff here
        log.Println(r.RequestURI)
        // Call the next handler, which can be another middleware in the chain, or the final handler.
        next.ServeHTTP(w, r)
    })
}
Run Code Online (Sandbox Code Playgroud)

路由定义如下:

r := mux.NewRouter()
r.HandleFunc("/", handler)
r.Use(loggingMiddleware)
Run Code Online (Sandbox Code Playgroud)

r.Use不注册网址路由的目的是什么?如何handler使用?

当我这样编写代码时,没有编译错误,但是我不明白我的函数应该如何写回“ Hello”。我想我可能w.Write在错误的地方使用。

mko*_*iva 9

我认为您可能将“中间件”与实际处理程序混合在一起。

http处理程序

实现该ServeHTTP(w http.ResponseWriter, r *http.Request)方法的类型满足http.Handler接口的要求,因此可以将这些类型的实例用作该http.Handle函数或等效http.ServeMux.Handle方法的第二个参数。

一个例子可以使这一点更加清楚:

type myHandler struct {
    // ...
}

func (h myHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte(`hello world`))
}

func main() {
    http.Handle("/", myHandler{})
    http.ListenAndServe(":8080", nil)
}
Run Code Online (Sandbox Code Playgroud)

HTTP处理程序功能

带有签名的函数func(w http.ResponseWriter, r *http.Request)是http处理函数,可以http.Handler使用http.HandlerFunc类型将其转换为。请注意,签名与http.HandlerServeHTTP方法的签名相同。

例如:

func myHandlerFunc(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte(`hello world`))
}

func main() {
    http.Handle("/", http.HandlerFunc(myHandlerFunc))
    http.ListenAndServe(":8080", nil)
}
Run Code Online (Sandbox Code Playgroud)

该表达式http.HandlerFunc(myHandlerFunc)myHandlerFunc函数转换为http.HandlerFunc实现该ServeHTTP方法的类型,因此该表达式的结果值是有效的http.Handler,因此可以将其http.Handle("/", ...)作为第二个参数传递给函数调用。

使用普通的http处理函数而不是实现该ServeHTTP方法的http处理程序类型很常见,因此标准库提供了替代方法http.HandleFunchttp.ServeMux.HandleFunc。所有HandleFunc操作都是我们在上面的示例中所做的事情,它将传入的函数转换为http.HandlerFunc并调用http.Handle结果。


http中间件

具有类似签名的功能func(h http.Handler) http.Handler被视为中间件。请记住,中间件的签名不受限制,您可以让中间件接受的参数比仅单个处理程序更多,并且还返回更多值,但通常来说,一个函数至少需要一个处理程序并重新运行至少一个新的处理程序可以视为中间件。

作为一个例子看看http.StripPrefix


现在让我们清除一些明显的混淆。

#1

func (s *Server) HandleSayHello(h http.Handler) http.Handler {
Run Code Online (Sandbox Code Playgroud)

方法的名称以及您之前使用它的方式,将其直接传递给HandleFunc,建议您将其设为普通的http处理函数,但是签名是中间件的签名,这就是导致错误的原因:

cannot use s.HandleSayHello (type func(http.Handler) http.Handler) as type func(http.ResponseWriter, *http.Request) in argument to s.Router.HandleFunc
Run Code Online (Sandbox Code Playgroud)

因此,将您的代码更新为类似于以下代码的代码将摆脱该编译错误,并且"Hello."在访问时也将正确呈现文本/sayhello

func (s *Server) HandleSayHello(w http.ResponseWriter, r *http.Request) {
      w.Write([]byte("Hello."))
}

func (s *Server) Routes(){
    s.Router.HandleFunc("/sayhello", s.HandleSayHello)
}
Run Code Online (Sandbox Code Playgroud)

#2

目前,我将只返回404调用 localhost:8091/sayhello

问题出在这两行

http.Handle("/sayhello", s.HandleSayHello(s.Router))
Run Code Online (Sandbox Code Playgroud)

http.ListenAndServe(":"+config.ServerPort, s.Router)
Run Code Online (Sandbox Code Playgroud)

http.Handle与处理程序通过FUNC寄存器的默认ServeMux情况下,它不会与大猩猩路由器实例注册它s.Router,你似乎认为,然后要传递s.RouterListenAndServe它使用它服务于每一个请求正在添加到FUNC localhost:8091,由于s.Router没有向其注册处理程序,您得到404


#3

但是现在如何设置路由时阻止实际功能执行?将"before"在我的打印语句显示了服务器开始之前。

func (s *Server) Routes(){
   http.Handle("/sayhello", s.HandleSayHello(s.Router))
}

func (s *Server) HandleSayHello(h http.Handler) http.Handler {
   log.Println("before")
   return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request){
      w.Write([]byte("Hello."))
      h.ServeHTTP(w, r)
   })
}
Run Code Online (Sandbox Code Playgroud)

取决于“实际功能”的含义。在Go中,您可以通过在函数名称的末尾添加括号来执行函数。因此,在设置路由时,此处执行的是http.Handle函数和HandleSayHello方法。

HandleSayHello方法的主体中本质上有两个语句,即function-call-expression语句log.Println("before")和return语句return http.HandlerFunc(...,这两个语句将在每次调用时执行HandleSayHello。但是,当您调用时,将不会执行返回的函数(处理程序)中的语句HandleSayHello,而是将在调用返回的处理程序时执行它们。

您不想"before"HandleSayHello被调用时被打印,但是想要在返回的处理程序被调用时被打印?您需要做的就是将日志行向下移动到返回的处理程序中:

func (s *Server) HandleSayHello(h http.Handler) http.Handler {
   return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request){
      log.Println("before")
      w.Write([]byte("Hello."))
      h.ServeHTTP(w, r)
   })
}
Run Code Online (Sandbox Code Playgroud)

现在,此代码当然毫无意义,即使作为教育目的的示例,它也会混淆而不是澄清处理程序和中间件的概念。

相反,可以考虑这样的事情:

// the handler func
func (s *Server) HandleSayHello(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello."))
}

// the middleware
func (s *Server) PrintBefore(h http.Handler) http.Handler {
       return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request){
               log.Println("before") // execute before the actual handler
               h.ServeHTTP(w, r)     // execute the actual handler
       })
}

func (s *Server) Routes(){
        // PrintBefore takes an http.Handler but HandleSayHello is an http handler func so
        // we first need to convert it to an http.Hanlder using the http.HandlerFunc type.
        s.Router.HandleFunc("/sayhello", s.PrintBefore(http.HandlerFunc(s.HandleSayHello)))
}
Run Code Online (Sandbox Code Playgroud)

#4

r := mux.NewRouter()
r.HandleFunc("/", handler)
r.Use(loggingMiddleware)
Run Code Online (Sandbox Code Playgroud)

r.Use不注册网址路由的目的是什么?如何handler使用?

Use 在路由器级别注册中间件,这意味着在该路由器上注册的所有处理程序将在执行中间件之前先执行中间件。

例如,上面的代码与此等效:

r := mux.NewRouter()
r.HandleFunc("/", loggingMiddleware(handler))
Run Code Online (Sandbox Code Playgroud)

当然Use,这并不是不必要的和令人困惑的,如果您有许多端点都具有不同的处理程序,并且它们全部都需要一堆中间件来应用,这很有用。

然后像这样的代码:

r.Handle("/foo", mw1(mw2(mw3(foohandler))))
r.Handle("/bar", mw1(mw2(mw3(barhandler))))
r.Handle("/baz", mw1(mw2(mw3(bazhandler))))
// ... hundreds more
Run Code Online (Sandbox Code Playgroud)

可以从根本上简化:

r.Handle("/foo", foohandler)
r.Handle("/bar", barhandler)
r.Handle("/baz", bazhandler)
// ... hundreds more
r.Use(mw1, mw2, m3)
Run Code Online (Sandbox Code Playgroud)


nov*_*ung 5

来自gorilla mux doc 文件:

中间件(通常)是一小段代码,它们接受一个请求,用它做一些事情,然后把它传递给另一个中间件或最终的处理程序。

r.Use()是用于登记一个中间件是有用的。您可以注册尽可能多的中间件。

r.HandleFunc("/hello", func (w http.ResponseWriter, r *http.Request) {
    fmt.Println("from handler")
    w.Write([]byte("Hello! \n"))
})

r.Use(func (next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // do something here
        fmt.Println("from middleware one")
        next.ServeHTTP(w, r)
    })
})

r.Use(func (next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // do another thing here
        fmt.Println("from middleware two")
        next.ServeHTTP(w, r)
    })
})

r.Use(func (next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // do something again but differently here
        fmt.Println("from middleware three")
        next.ServeHTTP(w, r)
    })
})
Run Code Online (Sandbox Code Playgroud)

如果你看到上面的代码,在每个中间件上都有 statement next.ServeHTTP(w, r)。该语句用于将传入的请求处理到下一步(它可以是下一个中间件,也可以是实际的处理程序)。

每个中间件将始终在实际处理程序之前执行。执行本身按顺序发生,具体取决于中间件注册的顺序。

在所有中间件成功执行后,next.ServeHTTP(w, r)最后一个中间件的 将处理传入请求以转到实际处理程序(在上面的示例中,它是/hello路由的处理程序)。

当您访问 时/hello,日志将打印:

from middleware one
from middleware two
from middleware three
from handler
Run Code Online (Sandbox Code Playgroud)

如果您希望在某些条件下不会继续传入请求,则只需不要调用next.ServeHTTP(w, r). 例子:

r.Use(func (next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // ...

        if someCondition {
            next.ServeHTTP(w, r)
        } else {
            http.Error(w, "some error happen", http.StatusBadRequest)
        }
    })
})
Run Code Online (Sandbox Code Playgroud)

中间件通常用于在调用处理程序之前或之后对传入请求执行某些处理。例如:CORS 配置、CRSF 检查、gzip 压缩、日志记录等。