首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >理解Go中的http handlerfunc包装技术

理解Go中的http handlerfunc包装技术
EN

Stack Overflow用户
提问于 2018-12-08 01:12:36
回答 2查看 21.1K关注 0票数 9

我看到了一个关于如何使用服务器类型的Mat Ryer撰写的文章func(http.ResponseWriter, *http.Request)包装器类型的http处理程序。

我认为这是构建REST的一种更优雅的方法,但是我很难让包装器正确地工作。我要么在编译时得到不匹配的类型错误,要么在调用时得到404。

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

代码语言:javascript
复制
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,所以我将处理程序设置为:

代码语言:javascript
复制
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

我在这是如此的帖子的解决方案中看到,我应该使用http.Handle并在路由器中传递。

代码语言:javascript
复制
func (s *Server) Routes(){
   http.Handle("/sayhello", s.HandleSayHello(s.Router))
}

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

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

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

这一资源展示了处理程序是如何编写的,而不是路由是如何设置的。

我确实发现Gorilla为这些东西提供了内置包装,但是我很难理解这个API。

他们展示的例子如下:

代码语言:javascript
复制
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)
    })
}

路线的定义如下:

代码语言:javascript
复制
r := mux.NewRouter()
r.HandleFunc("/", handler)
r.Use(loggingMiddleware)

r.Use没有注册url路由时,它的目的是什么?handler是如何使用的?

当我的代码像这样写的时候,我不会收到编译错误,但是我不明白我的函数是如何回写"Hello“的。我想我可能在错误的地方使用了w.Write

EN

回答 2

Stack Overflow用户

发布于 2018-12-08 10:44:38

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

http处理程序

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

举个例子可以更清楚地说明这一点:

代码语言:javascript
复制
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.HandlerFunc。注意,签名与http.HandlerServeHTTP方法的签名相同。

例如:

代码语言:javascript
复制
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,因此它可以作为第二个参数传递给http.Handle("/", ...)函数调用。

使用普通的http处理程序函数而不是实现ServeHTTP方法的http处理程序类型非常常见,以至于标准库提供了可供选择的http.HandleFunchttp.ServeMux.HandleFuncHandleFunc所做的一切都是我们在上面的示例中所做的,它将传递的函数转换为http.HandlerFunc,并使用结果调用http.Handle

http中间件

具有类似于此func(h http.Handler) http.Handler的签名的函数被认为是中间件。请记住,中间件的签名不受限制,您可以让中间件接受比单个处理程序更多的参数,并返回更多的值,但通常,至少需要一个处理程序和retruns的函数--至少一个新的处理程序--可以被认为是中间件。

举个例子,看看http.StripPrefix

现在让我们澄清一些表面上的混乱。

#1

代码语言:javascript
复制
func (s *Server) HandleSayHello(h http.Handler) http.Handler {

方法的名称和以前使用它的方式,直接传递给HandleFunc,表明您希望它是一个普通的http处理程序函数,但是签名是中间件的签名,这就是您得到错误的原因:

代码语言:javascript
复制
cannot use s.HandleSayHello (type func(http.Handler) http.Handler) as type func(http.ResponseWriter, *http.Request) in argument to s.Router.HandleFunc

因此,将代码更新为如下代码将消除编译错误,并在访问"Hello."时正确地呈现/sayhello文本。

代码语言:javascript
复制
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

问题就在这两行

代码语言:javascript
复制
http.Handle("/sayhello", s.HandleSayHello(s.Router))

代码语言:javascript
复制
http.ListenAndServe(":"+config.ServerPort, s.Router)

http.Handle func向默认ServeMux实例注册传入处理程序,它不像您所假设的那样将它注册到s.Router中的大猩猩路由器实例中,然后您将s.Router传递给ListenAndServe func,后者使用它来服务每个启动到localhost:8091的请求,而且由于s.Router没有向它注册的处理程序,所以可以得到404

#3

但是,现在如何在设置路由时阻止实际函数执行呢?在服务器启动之前,打印语句中的"before"就会出现。

代码语言:javascript
复制
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时执行。但是,在调用HandleSayHello时,将不会执行返回函数(即处理程序)中的语句,而是在调用返回的处理程序时执行这些语句。

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

代码语言:javascript
复制
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)
   })
}

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

相反,不妨考虑一下这样的事情:

代码语言:javascript
复制
// 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

代码语言:javascript
复制
r := mux.NewRouter()
r.HandleFunc("/", handler)
r.Use(loggingMiddleware)

r.Use没有注册url路由时,它的目的是什么?handler是如何使用的?

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

例如,上面的代码相当于:

代码语言:javascript
复制
r := mux.NewRouter()
r.HandleFunc("/", loggingMiddleware(handler))

当然,Use并不是不必要的和令人困惑的,如果您有许多端点--所有这些端点都有不同的处理程序,而且所有这些端点都需要一堆中间件来应用于它们,这是非常有用的。

然后编写如下代码:

代码语言:javascript
复制
r.Handle("/foo", mw1(mw2(mw3(foohandler))))
r.Handle("/bar", mw1(mw2(mw3(barhandler))))
r.Handle("/baz", mw1(mw2(mw3(bazhandler))))
// ... hundreds more

可以从根本上简化:

代码语言:javascript
复制
r.Handle("/foo", foohandler)
r.Handle("/bar", barhandler)
r.Handle("/baz", bazhandler)
// ... hundreds more
r.Use(mw1, mw2, m3)
票数 25
EN

Stack Overflow用户

发布于 2018-12-08 01:31:20

来自大猩猩文档文件:

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

r.Use()对于注册中间件非常有用。您可以注册尽可能多的中间件。

代码语言:javascript
复制
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时,日志将打印:

代码语言:javascript
复制
from middleware one
from middleware two
from middleware three
from handler

如果您希望在特定的条件下,传入的请求不会继续进行,那么就不要调用next.ServeHTTP(w, r)。示例:

代码语言:javascript
复制
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压缩、日志记录等。

票数 7
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/53678633

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档