【问题标题】:Understanding the http handlerfunc wrapper technique in Go理解 Go 中的 http handlerfunc 包装器技术
【发布时间】:2019-05-09 18:10:36
【问题描述】:

我看到了一个article written by Mat Ryer,关于如何使用服务器类型和作为func(http.ResponseWriter, *http.Request) 包装器类型的http 处理程序

我认为这是构建 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)
}

因为现在是这样,我只会返回一个 404 调用 localhost:8091/sayhello。 (是的,这是我在配置文件中设置的端口。)

之前,由于我使用的是 Gorilla Mux,因此我将处理程序设置为:

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

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

我在this SO post 的解决方案中看到我应该使用http.Handle 并传入路由器。

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

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

Researching 这种技术further,我发现其他读数表明我需要定义middlewarehandler 类型。

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

This resource 显示处理程序的编写方式,但不显示路由的设置方式。

我确实发现 Gorilla Mux 有 built in wrappers 用于这些东西,但我很难理解 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)
    })
}

路由是这样定义的:

r := mux.NewRouter()
r.HandleFunc("/", handler)
r.Use(loggingMiddleware)

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

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

【问题讨论】:

    标签: rest http go wrapper gorilla


    【解决方案1】:

    我认为您可能将“中间件”与真正的处理程序混为一谈。

    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)
    }
    

    http 处理函数

    签名为func(w http.ResponseWriter, r *http.Request) 的函数是http 处理函数,可以使用http.HandlerFunc 类型转换为http.Handler。注意签名和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)
    }
    

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

    使用普通的 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 {
    

    方法的名称和你之前使用的方式,直接传给HandleFunc,建议你希望这是一个普通的http handler func,但是签名是中间件的,这就是原因你得到的错误:

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

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

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

    #2

    因为现在是这样,我只会返回 404 调用 localhost:8091/sayhello.

    问题出在这两行

    http.Handle("/sayhello", s.HandleSayHello(s.Router))
    

    http.ListenAndServe(":"+config.ServerPort, s.Router)
    

    http.Handle 函数将传入的处理程序注册到default ServeMux instance,它没有像您想象的那样将它注册到s.Router 中的大猩猩路由器实例,然后您将s.Router 传递给@ 987654370@ 函数,它使用它来处理发送到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)
       })
    }
    

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

    HandleSayHello 方法的主体中有两个语句,函数调用表达式语句log.Println("before") 和返回语句return http.HandlerFunc(...,每次调用HandleSayHello 时都会执行这两个语句。但是,返回函数handler中的语句在调用HandleSayHello时不会执行,而是在调用返回的handler时执行。

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

    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)
       })
    }
    

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

    也许可以考虑这样的事情:

    // 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)))
    }
    

    #4

    r := mux.NewRouter()
    r.HandleFunc("/", handler)
    r.Use(loggingMiddleware)
    

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

    Use 在路由器级别注册中间件,这意味着向该路由器注册的所有处理程序都将在执行它们自己之前执行中间件。

    例如上面的代码等价于:

    r := mux.NewRouter()
    r.HandleFunc("/", loggingMiddleware(handler))
    

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

    然后代码如下:

    r.Handle("/foo", mw1(mw2(mw3(foohandler))))
    r.Handle("/bar", mw1(mw2(mw3(barhandler))))
    r.Handle("/baz", mw1(mw2(mw3(bazhandler))))
    // ... hundreds more
    

    可以从根本上简化:

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

    【讨论】:

    • 只关注#1,我试图了解是什么让某些东西满足了ServerHTTP的要求。 http.Handle 只能注册 http.HandlerFunc 类型的函数,对吧?注册一个返回该类型的函数不起作用吗?我试图尽可能地遵循 Mat Ryer 的示例,因为我有许多处理程序依赖项,我想将其作为参数传递给处理函数。
    • @GhostRavenstorm 不,http.Handle 采用实现http.Handler 接口的任何类型的值,http.HandlerFunc 是实现该接口的类型之一,但它不是唯一的。所以你的 "http.Handle can only register functions that are http.HandlerFunc type" 语句不正确。
    • 好的,我理解的更清楚了。我尝试让 handlerfunc 返回一个 handlerfunc,但我仍然收到类型错误。在HandleFunc 中传递的函数似乎必须与ServerHTTP(w http.ResponseWriter, r *http.Request) 的签名完全匹配,否则会导致问题。在那种情况下,我不明白this post 是如何使用有效代码的。这些示例似乎有一些切入点,但没有显示我缺少让这些示例工作。
    • @GhostRavenstorm:“注册一个返回该类型的函数不起作用吗?” 再次,不。如果函数F 接受T 类型的参数,那么你必须传入T 类型的值,你不能传入其他任何东西;仅仅传递一个返回T 的函数是不够的。无论你在 Mat Ryer 的文章中读到什么,你显然都误解了,所以我建议你再读几遍。为了清楚起见,我不是想侮辱你,我必须阅读无数文章,直到我能够围绕界面进行思考。
    • @GhostRavenstorm 你在这里是对的,http.HandleFunc 接受http.HandlerFunc 类型的函数。请注意http.HandleFunchttp.Hanlde 不同。
    【解决方案2】:

    来自gorilla mux 文档文件:

    中间件(通常)是一小段代码,它们接受一个请求,对其进行处理,然后将其传递给另一个中间件或最终处理程序。

    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)
        })
    })
    

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

    每个中间件总是在实际处理程序之前执行。它自己的执行按顺序发生,具体取决于中间件注册的顺序。

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

    当你访问/hello时,日志会打印出来:

    from middleware one
    from middleware two
    from middleware three
    from handler
    

    如果您希望在某些情况下不会继续传入请求,那么就不要调用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)
            }
        })
    })
    

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

    【讨论】:

    • 这很棒。我明白 Use 现在做了什么。不幸的是,这不是我正在寻找的解决方案。我正在尝试编写不处理请求的处理程序,但返回根据 Mat Ryer 的帖子 medium.com/statuscode/… 执行的函数我正在尝试使用我正在使用的路由器 Gorilla Mux 来完成它。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-11-23
    • 2018-11-11
    • 1970-01-01
    相关资源
    最近更新 更多