Skip to content

Files

Latest commit

d71eae6 · Jan 10, 2022

History

History
567 lines (380 loc) · 20.8 KB

3.md

File metadata and controls

567 lines (380 loc) · 20.8 KB

二、服务和路由

Go 的net包方便了 Go 程序中的所有网络通信,无论是通过 HTTP、TCP/IP、WebSockets 还是任何其他标准网络协议。

当然,因为这本书是针对网络开发的,所以我们主要关注的是 HTTP,这意味着我们将使用的主要子包是net/http

在本章中,我们将了解任何 Go web 应用程序的基本需求——服务和路由——以及如何使用net/http和补充包来实现它们。

作为一个简单的网络服务器

回到过去,web 服务器只不过是提供驻留在服务器目录中的文件。如果这就是我们现在想要的,我们可以在几行代码中使用net/http package在 Go 中做一些非常相似的事情。

代码清单 1:作为网络服务器运行

  package
  main

  import
  (
        "net/http"
  )

  func
  main() {
        http.ListenAndServe(":8999",
                              http.FileServer(http.Dir("/var/www")))
  }

这是非常基本的,但是我们的程序履行了任何 web 服务器的核心职责——即,倾听请求和提供响应。更重要的是,因为它不必担心所有其他问题,一个更传统的网络服务器必须,程序是闪电般的速度。

在这个例子中,我们调用http.ListenAndServe()函数将端口 8999 上的所有请求发送到http.FileServer处理程序方法,该方法依次接受服务器上我们想要提供文件的目录。放轻松!

如果您使用 go run <*program_name.go*>运行程序,并在浏览器的地址栏中输入http://localhost:8999/,后跟服务器上存在于/var/www中的一个文件的名称,您将看到该文件的内容被显示出来,如图 4 所示。

图 4:去服务地鼠

然而,大多数现代 web 应用程序对服务器的要求比这多一点。请记住,您不会总是发送静态内容。您将越来越多地被要求动态地生成内容,也许是从数据库的内容中。在这种情况下,物理文件位置没有多大意义。

假设您有一个更复杂的站点结构,其中应用程序的文件分布在不同的子目录中?比如www.mysite.com/about/aboutus.htmlwww.mysite.com/blog/blog.html?这种方法行不通。因此,您需要更好地控制应用程序可以接受的网址。您可以通过使用net/http包的路由功能来实现这一点。

简单的上菜和路由

Go 依赖两个主要组件来处理 HTTP 请求——多路复用器和处理程序。多路复用器(或“多路复用器”)本质上是一个 HTTP 请求路由器。在围棋的net/http包中,多路复用器功能由ServeMux提供,默认的发球多路复用器是DefaultServeMux

直觉,嗯?

ServeMux将传入的请求与预定义的网址路径列表进行比较,然后在匹配时为每个路径调用适当的处理程序(您定义的函数)。

让我们首先看一下代码,然后我们将检查发生了什么。

代码清单 2:使用网络/http 的基本服务和路由

  package
  main

  import
  (
        "fmt"
        "html"
        "log"
        "net/http"
        "time"
  )

  func
  main() {
        http.HandleFunc("/", showInfo)
        http.HandleFunc("/site", serveFile)
        err
  := http.ListenAndServe(":8999", nil)
        if err != nil {
              log.Fatal("ListenAndServe: ", err)
        }
  }

  func
  showInfo(w http.ResponseWriter, r *http.Request)
  {
        fmt.Fprintln(w,
  "Current time: ", time.Now())
        fmt.Fprintln(w,
  "URL Path: ",
  html.EscapeString(r.URL.Path))
  }

  func
  serveFile(w http.ResponseWriter, r *http.Request)
  {
        http.ServeFile(w,
  r, "index.html")
  }

请注意,main()函数是程序的入口点,它通过使用http.HandleFunc(*route, handler*)方法将网址路由映射到一个函数来设置一些服务器路由,该函数将响应任何与该路由匹配的请求。

程序接下来调用http.ListenAndServe()方法,该方法用指定的地址(在本例中是端口 8999 上的本地机器)和多路复用器启动一个 HTTP 服务器。这种情况下的多路复用器是nil,它告诉 Go 使用DefaultServeMux

根据访问的网址是在网站的根目录(“/”)还是在“/site”,处理函数会做不同的事情。然而,在这两种情况下,它们的方法签名必须同时实现http.ResponseWriterhttp.Request。如果一个没有实现这两者的处理程序通过http.HandleFunc()被调用,你将会看到 Go 将会引发一个编译时错误。

如果用户访问/site,则http.ServeFile()方法处理该请求并返回与应用程序位于同一目录的index.html页面。您可以使用http.ServeFile()发送响应中的任何静态文件。

如果用户访问根位置,将调用showInfo(),并显示当前时间以及用户在根网址之外输入的任何路径。应用程序通过使用传递给处理程序的http.Request对象的URL.Path属性来提取这些信息。响应由http.ResponseWriter生成。

通过从您的 IDE 发出go run <*program_name*.*go*>或简单地通过命令行启动程序。接下来,在浏览器中访问localhost:8999。尝试各种组合来测试它的功能。

例如,输入http://localhost:8999会产生图 5 中动态生成的页面。

图 5:访问网络应用程序的根目录

输入任何子路线(例如,localhost:8999/hello/there/from/golang)会显示输入的时间和日期以及子路线,如图 6 所示。

图 6:查看子路径

进入localhost:8999/site显示index.html页面。

图 7:访问/站点

在这个例子中,让我们扩展服务静态文件的能力。假设请求中的 URL 以/static/开头,我们想要剥离 URL 的/static部分,以便在/var/www目录的剩余路径中查找引用的文件。我们可以使用StripPrefix函数来实现这一点,如代码清单 3 所示。

代码清单 3:使用 StripPrefix

  package
  main

  import
  (
        "fmt"
        "html"
        "log"
        "net/http"
        "time"
  )

  func
  main() {
        http.HandleFunc("/", showInfo)
        files
  := http.FileServer(http.Dir("/var/www"))
        http.Handle("/site/", http.StripPrefix("/site/", files))
        err
  := http.ListenAndServe(":8999", nil)
        if err != nil {
              log.Fatal("ListenAndServe: ", err)
        }
  }

  func
  showInfo(w http.ResponseWriter, r *http.Request)
  {
        fmt.Fprintln(w,
  "Current time: ", time.Now())
        fmt.Fprintln(w,
  "URL Path: ",
  html.EscapeString(r.URL.Path))
  }

请注意,在本例中,我们并未调用HandleFunc,而是调用Handle。这是因为FileServer函数返回它自己的处理程序,我们可以使用Handle将它传递给 mux(而不是显式创建我们自己的处理程序函数)。

因此,您可以看到,让一个自助式网站(不需要单独的 web 服务器)开始运行并让您响应一些简单的请求是很简单的。

中间件

在 web 开发中,中间件是位于 web 请求和路由处理器之间的代码。中间件由可重用的代码组成,您可以使用这些代码来执行必须在调用处理程序之前或之后发生的任务。

“中间件”一词经常用于 Go 编程,但您也可能会看到其他 web 语言和技术使用的类似术语,例如“拦截器”、“挂钩”和“请求过滤”

例如,您可能希望在路由请求之前检查数据库连接的状态或对用户进行身份验证。您可能还想压缩响应的内容或限制调用特定处理程序的次数,这可能是您对免费访问您的 web 服务的用户施加的一些限制的一部分。

创建中间件只是链接处理程序和处理程序函数的问题,这是您将在 Go web 开发中经常看到和使用的东西。

基本思想是将一个处理函数——我们称之为F2——作为参数传递给另一个处理函数——我们称之为这个 f1 。处理程序 f1 在触发它的路线被访问时被调用。 f1 做一些工作,然后调用 f2

当然,你可以让 f1 直接呼叫 f2 。然而,这并不理想,因为我们通常希望实现明确的关注点分离,因此我们的处理程序代码应该真正限于处理请求,而不是做 f2 设计要做的任何事情。

这里有一个基本的想法——不是把响应作为处理程序的一部分来处理,而是简单地把链中的下一个处理程序传递给它。

接受另一个处理函数作为参数并返回新函数的函数可以在调用处理函数之前或之后执行任务(或者前后都执行),甚至可以最终选择完全不调用原始处理函数(如果这是您的意图)。

考虑代码清单 4 中的例子。

代码清单 4:中间件示例 1

  package
  main

  import
  (
        "fmt"
        "net/http"
  )

  func
  middleware1(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
              fmt.Fprintln(w,
  "Executing middleware1()...")
              next.ServeHTTP(w,
  r)
              fmt.Fprintln(w,
  "Executing middleware1() again...")
        })
  }

  func
  middleware2(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
              fmt.Fprintln(w,
  "Executing middleware2()...")
              if r.URL.Path !=
  "/" {
                    return
              }
              next.ServeHTTP(w,
  r)
              fmt.Fprintln(w,
  "Executing middleware2() again...")
        })
  }

  func
  final(w http.ResponseWriter, r *http.Request)
  {
        fmt.Fprintln(w, "Executing final()...")
        fmt.Fprintln(w,
  "Done")
  }

  func
  main() {
        finalHandler
  := http.HandlerFunc(final)

        http.Handle("/",
  middleware1(middleware2(finalHandler)))
        http.ListenAndServe(":8999", nil)
  }

main()功能。这里,我们用middleware1处理程序拦截对我们的根网络目录的请求。该处理程序接受另一个名为middleware2的处理程序作为参数。这又将final处理程序作为参数。

当有人访问我们的网络应用程序时,它会调用middleware1,显示一条消息,当它点击next.serveHTTP时,在middleware2执行代码。middleware2功能依次调用final,执行后控制返回middleware2,T5 完成任务后控制返回middleware1

输出将如图 8 所示。

图 8:中间件示例 1 的输出

图 8 提供了一个完全人为的例子,但是它用来说明中间件能给你多少控制。

Go 内部经常使用中间件。例如,net/http包中的许多函数,比如StripPrefix,都是中间件的教科书式例子——它们包装你的处理程序,并对请求或响应执行额外的操作。

中间件的概念可能有些难以理解,所以代码清单 5 显示了另一个例子。

代码清单 5:中间件示例 2

  package
  main

  import
  (
        "net/http"
  )

  type
  AfterMiddleware struct {
        handler
  http.Handler
  }

  func (a
  *AfterMiddleware) ServeHTTP(w
  http.ResponseWriter, r *http.Request) {
        a.handler.ServeHTTP(w,
  r)
        w.Write([]byte(" +++ Hello
  from middleware! +++ "))
  }

  func
  myHandler(w http.ResponseWriter, r *http.Request)
  {
        w.Write([]byte(" *** Hello
  from myHandler! *** "))
  }

  func
  main() {
        mid
  := &AfterMiddleware{http.HandlerFunc(myHandler)}

        println("Listening
  on port 8999")
        http.ListenAndServe(":8999", mid)
  }

在这段代码中,我们希望我们的中间件在myHandler函数写入响应体时执行,并向其中添加一些数据。

首先,我们为中间件创建一个类型,我们称之为AfterMiddleware。这由一个字段组成——我们希望我们的中间件响应的http.Handler

http.Handler接口只需要我们实现ServeHTTP接口:

type Handler interface {

ServeHTTP(ResponseWriter, *Request)

}

我们按如下方式进行:

func (a *AfterMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {

a.handler.ServeHTTP(w, r)

w.Write([]byte(" +++ Hello from middleware! +++ "))

}

现在,响应由myHandler所写的内容组成,后面是图 9 中中间件的输出。

图 9:中间件示例 2 的输出

这里我们不能涵盖中间件的所有方面,但是它是一个非常强大的特性,非常值得研究。Mat Ryer 的这篇文章和视频讲座是对该主题的极好介绍。

当我们创建一些非常有用的日志中间件时,我们将在第 7 章重新讨论中间件的概念。

大猩猩网络工具包提供更高级的服务和路由

Go 内置的net/http包提供的路由功能只能让你做到这一步。如果可能的网址范围很复杂,例如,如果您希望能够使用正则表达式或变量来匹配网址,您可能会考虑第三方解决方案。

大猩猩网络工具包就是这样一个解决方案。大猩猩由 22 个包装组成,包括:

  • gorilla/context存储全局请求变量。
  • gorilla/mux是功能强大的 URL 路由器和调度器。
  • gorilla/reverse为基于正则表达式的 mux 生成可逆的正则表达式。
  • gorilla/rpc通过 JSON-RPC 的编解码器实现 HTTP 上的 RPC。
  • gorilla/schema将表单值转换为结构。
  • gorilla/securecookie编码和解码经过身份验证和可选加密的 cookie 值。
  • gorilla/sessions保存 cookie 和文件系统会话,并允许自定义会话后端。
  • gorilla/websocket实现 RFC 6455 中定义的 WebSocket 协议。

从包列表中可以看出,Gorilla 不仅仅是 Go 路由和服务的替代多路复用器。事实上,它提供了一系列不同的工具来帮助您进行 web 开发。

但在这里,我们对多路复用器感兴趣。gorilla/mux模块实现http.Handler接口,使其与标准的 Golang 兼容。http.serveMux

此外,gorilla/mux模块使您能够:

  • 基于 URL 主机、路径、路径前缀、方案、头和查询值、HTTP 方法或您定义的自定义匹配器来匹配请求。
  • 注册网址,以便您可以构建或“反转”它们,从而维护对资源的引用。
  • 使用“子计算机”——仅当父路由匹配时才测试的嵌套路由。通过这种方式,您可以定义路由的“组”,这些路由都有一些共同点,例如主机或路径前缀。这通过仅在某些测试适合组的情况下进行测试来优化请求匹配(而不是对所有传入的请求执行测试)。

安装和参考大猩猩/多路复用器包

如果你只对 Gorilla 提供的更高级的路由功能感兴趣,你可以使用go get在你的GOPATH中安装gorilla/mux。例如,假设您安装了git :

go get github.com/gorilla/mux

接下来,您必须将其导入到应用程序中,如下所示:

package main

import ( github.com/gorilla/mux ... )

使用大猩猩/多路复用器

为了好玩,让我们使用gorilla/mux创建一个基于正则表达式匹配产品标识的传入请求的处理程序。如果产品标识是一位数长,我们有一个匹配,可以路由到适当的页面。如果没有,我们将路由到错误页面。

我们首先需要导入gorilla/mux模块,这是以正常方式进行的:

import( . . . github.com/gorilla/mux )

接下来,我们需要告诉 Go 使用gorilla/mux而不是它自己的DefaultServeMux:

func main() { router = mux.NewRouter() }

完成后,我们可以使用我们熟悉的处理函数,但是要在router的上下文中使用gorilla/mux提供的额外功能,例如使用正则表达式搜索 URL 参数:

router.HandleFunc("/product/{id:[0-9]+}", pageHandler)

这一行代码创建了一个处理函数,该函数试图匹配一个由从 0 到 9(包括 0 和 9)的数字/product/组成的 URL,它称之为id。如果有匹配,则调用pageHandler函数。

在我们的pageHandler函数中,我们需要一种方法来检查HandleFunc匹配的确切字符串。我们可以使用 Gorilla 的mux.Vars函数来做到这一点,将请求作为单个参数传入,并获取路线变量的映射。我们可以通过名称来引用我们感兴趣的路由变量— id

检索到产品标识后,我们接下来可以检查匹配的 HTML 页面。Go 的os模块提供了StatIsNotExist功能来帮助我们。如果文件存在于服务器上,我们使用http.ServeFile将其发送到浏览器。如果那里不存在,我们将返回另一个页面,告诉用户产品无效。

代码清单 6 显示了完整的代码。

代码清单 6:使用 gorilla/mux 服务和路由

  package
  main

  import
  (
        "log"
        "net/http"
        "os"

        "github.com/gorilla/mux"
  )

  func
  pageHandler(w http.ResponseWriter, r *http.Request)
  {
        vars :=
  mux.Vars(r)
        productID
  := vars["id"]
        log.Printf("Product ID:%v\n", productID)

        fileName
  := productID +
  ".html"

        if _, err :=
  os.Stat(fileName); os.IsNotExist(err) {
              log.Printf("no such product")
              fileName = "invalid.html"
        }

        http.ServeFile(w,
  r, fileName)
  }

  func
  main() {
        router
  := mux.NewRouter()
        router.HandleFunc("/product/{id:[0-9]+}", pageHandler)
        http.Handle("/", router)
        http.ListenAndServe(":8999", nil)
  }

这只是gorilla/mux能做的事情的表面。例如,您可以匹配:

路径前缀:

router.PathPrefix("/products/")

HTTP 方法:

router.Methods("GET", "POST")

网址方案:

router.Schemes("https")

头值(例如,请求是 AJAX 请求吗?):

router.Headers("X-Requested-With", "XMLHttpRequest")

查询值:

router.Queries("key", "value")

您定义的自定义匹配函数:

router.MatcherFunc(func(r *http.Request, match *RouteMatch) bool {

// do something

})

您也可以通过链接函数调用将多个匹配器组合在一条路径中:

router.HandleFunc("/products", productHandler).

Host("www.example.com").

Methods("GET").

Schemes("https")

或者,您可以使用子路由将多条路由组合在一起。例如,下面的代码寻找一个名为www.example.com的主机名,但是它只有在主机名匹配的情况下才会检查子路由:

router := mux.NewRouter()

subrouter := route.Host("www.example.com").Subrouter()

//注册子行

subrouter.HandleFunc("/products/", AllProductsHandler)

subrouter.HandleFunc("/products/{name}", ProductHandler)

subrouter.HandleFunc("/reviews/{category}/{id:[0-9]+}"), ReviewsHandler)

gorilla/mux包是DefaultServeMux的一个非常有能力的替代品,非常值得研究。您可以在http://www.gorillatoolkit.org/pkg/mux了解更多信息。

当然,围棋是一个充满活力的生态系统,如果你不喜欢大猩猩,还有很多其他选择。我更喜欢大猩猩,因为它很容易让我分心,它总是能做我让它做的任何事情。

其他流行的第三方路由器包括:

  • httprouter:快如闪电,但能力不及大猩猩。例如,您不能在路由中包含正则表达式。
  • Httptreemux:快速、灵活、基于树的 HTTP 路由器,灵感来自httprouter
  • Pat:使用简单,相当受欢迎。如果你是熟悉西纳特拉和 Rails 的 Ruby 主义者,你会发现 Pat 的方法非常熟悉。

返回错误

事情并不总是如我们所愿。对不再存在的页面进行请求,将东西从一个地方移动到另一个地方,有时连接会中断。

HTTP 协议定义了 61 个不同的状态代码,这样我们就可以确定一个请求是否成功。

以下是一些最常见的例子:

  • 200 OK:万岁!一切都好。
  • 404 Not found:无论你在找什么资源,在这里都找不到。
  • 301 Moved Permanently:用作永久重定向到另一个页面。
  • 301 Moved Temporarily:用作临时重定向到另一个页面。
  • 500 Internal Server Error:出现了意想不到的问题。这是一个“包罗万象”

net/http包提供了一个名为Error的功能,您可以使用它来处理错误状态代码。作为开发人员,你的工作是选择一个对你所报告的错误类型有意义的。

您可以通过传递ResponseWriter、字符串消息和状态代码来引发错误。例如,这会引发404 Not Found错误:

http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {

http.Error(w, "Something has gone wrong", 500)

})

然而,更好的做法是使用net/http为此目的提供的各种辅助函数,而不是通用的http.Error()函数。

例如:

// Return 404 Not Found http.NotFound(w, req)

`// Return 301 Permanently Moved http.Redirect(w, req, “http://somewhereelse.com”, 301)

// Return 302 Temporarily Moved http.Redirect(w, req “http://temporarylocation”, 302)`