Go Web 编程之 程序结构

概述

一个典型的 Go Web 程序结构以下,摘自《Go Web 编程》:golang

  • 客户端发送请求;
  • 服务器中的多路复用器收到请求;
  • 多路复用器根据请求的 URL 找到注册的处理器,将请求交由处理器处理;
  • 处理器执行程序逻辑,必要时与数据库进行交互,获得处理结果;
  • 处理器调用模板引擎将指定的模板和上一步获得的结果渲染成客户端可识别的数据格式(一般是 HTML);
  • 最后将数据经过响应返回给客户端;
  • 客户端拿到数据,执行对应的操做,如渲染出来呈现给用户。

本文介绍如何建立多路复用器,如何注册处理器,最后再简单介绍一下 URL 匹配。咱们以上一篇文章中的"Hello World"程序做为基础。数据库

package main

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

func hello(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello World")
}

func main() {
    http.HandleFunc("/", hello)
    if err := http.ListenAndServe(":8080", nil); err != nil {
        log.Fatal(err)
    }
}

多路复用器

默认多路复用器

net/http 包为了方便咱们使用,内置了一个默认的多路复用器DefaultServeMux。定义以下:编程

// src/net/http/server.go

// DefaultServeMux is the default ServeMux used by Serve.
var DefaultServeMux = &defaultServeMux

var defaultServeMux ServeMux

这里给你们介绍一下 Go 标准库代码的组织方式,便于你们对照。浏览器

  • Windows上,Go 语言的默认安装目录为C:\Go,即GOROOT
  • GOROOT下有一个 src 目录,标库库的代码都在这个目录中;
  • 每一个包有一个单独的目录,例如 fmt 包在src/fmt目录中;
  • 子包在其父包的子目录中,例如 net/http 包在src/net/http目录中。

net/http 包中不少方法都在内部调用DefaultServeMux的对应方法,如HandleFunc。咱们知道,HandleFunc是为指定的 URL 注册一个处理器(准确来讲,hello是处理器函数,见下文)。其内部实现以下:服务器

// src/net/http/server.go
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    DefaultServeMux.HandleFunc(pattern, handler)
}

实际上,http.HandleFunc方法是将处理器注册到DefaultServeMux中的。函数

另外,咱们使用 ":8080" 和 nil 做为参数调用http.ListenAndServe时,会建立一个默认的服务器:spa

// src/net/http/server.go
func ListenAndServe(addr string, handler Handler) {
    server := &Server{Addr: addr, Handler: handler}
    return server.ListenAndServe()
}

这个服务器默认使用DefaultServeMux来处理器请求:code

type serverHandler struct {
    srv *Server
}

func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
    handler := sh.srv.Handler
    if handler == nil {
        handler = DefaultServeMux
    }
    handler.ServeHTTP(rw, req)
}

服务器收到的每一个请求会调用对应多路复用器(即ServeMux)的ServeHTTP方法。在ServeMuxServeHTTP方法中,根据 URL 查找咱们注册的处理器,而后将请求交由它处理。server

虽然默认的多路复用器使用起来很方便,可是在生产环境中不建议使用。因为DefaultServeMux是一个全局变量,全部代码,包括第三方代码均可以修改它。
有些第三方代码会在DefaultServeMux注册一些处理器,这可能与咱们注册的处理器冲突。对象

比较推荐的作法是本身建立多路复用器。

建立多路复用器

建立多路复用器也比较简单,直接调用http.NewServeMux方法便可。而后,在新建立的多路复用器上注册处理器:

package main

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

func hello(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello World")
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", hello)

    server := &http.Server{
        Addr:    ":8080",
        Handler: mux,
    }

    if err := server.ListenAndServe(); err != nil {
        log.Fatal(err)
    }
}

上面代码的功能与 "Hello World" 程序相同。这里咱们还本身建立了服务器对象。经过指定服务器的参数,咱们能够建立定制化的服务器。

server := &http.Server{
    Addr:           ":8080",
    Handler:        mux,
    ReadTimeout:    1 * time.Second,
    WriteTimeout:   1 * time.Second,
}

在上面代码,咱们建立了一个读超时和写超时均为 1s 的服务器。

处理器和处理器函数

上文中提到,服务器收到请求后,会根据其 URL 将请求交给相应的处理器处理。处理器是实现了Handler接口的结构,Handler接口定义在 net/http 包中:

// src/net/http/server.go
type Handler interface {
    func ServeHTTP(w Response.Writer, r *Request)
}

咱们能够定义一个实现该接口的结构,注册这个结构类型的对象到多路复用器中:

package main

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

type GreetingHandler struct {
    Language string
}

func (h GreetingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "%s", h.Language)
}

func main() {
    mux := http.NewServeMux()
    mux.Handle("/chinese", GreetingHandler{Language: "你好"})
    mux.Handle("/english", GreetingHandler{Language: "Hello"})
    
    server := &http.Server {
        Addr:   ":8080",
        Handler: mux,
    }
    
    if err := server.ListenAndServe(); err != nil {
        log.Fatal(err)
    }
}

与前面的代码有所不一样,上段代码中,定义了一个实现Handler接口的结构GreetingHandler。而后,建立该结构的两个对象,分别将它注册到多路复用器的/hello/world路径上。注意,这里注册使用的是Handle方法,注意与HandleFunc方法对比。

启动服务器以后,在浏览器的地址栏中输入localhost:8080/chinese,浏览器中将显示你好,输入localhost:8080/english将显示Hello

虽然,自定义处理器这种方式比较灵活,强大,可是须要定义一个新的结构,实现ServeHTTP方法,仍是比较繁琐的。为了方便使用,net/http 包提供了以函数的方式注册处理器,即便用HandleFunc注册。函数必须知足签名:func (w http.ResponseWriter, r *http.Request)
咱们称这个函数为处理器函数。咱们的 "Hello World" 程序中使用的就是这种方式。HandleFunc方法内部,会将传入的处理器函数转换为HandlerFunc类型。

// src/net/http/server.go
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    if handler == nil {
        panic("http: nil handler")
    }
    mux.Handle(pattern, HandlerFunc(handler))
}

HandlerFunc是底层类型为func (w ResponseWriter, r *Request)的新类型,它能够自定义其方法。因为HandlerFunc类型实现了Handler接口,因此它也是一个处理器类型,最终使用Handle注册。

// src/net/http/server.go
type HandlerFunc func(w *ResponseWriter, r *Request)

func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}

注意,这几个接口和方法名很容易混淆,这里再强调一下:

  • Handler:处理器接口,定义在 net/http 包中。实现该接口的类型,其对象能够注册到多路复用器中;
  • Handle:注册处理器的方法;
  • HandleFunc:注册处理器函数的方法;
  • HandlerFunc:底层类型为func (w ResponseWriter, r *Request)的新类型,实现了Handler接口。它链接了处理器函数处理器

URL 匹配

通常的 Web 服务器有很是多的 URL 绑定,不一样的 URL 对应不一样的处理器。可是服务器是怎么决定使用哪一个处理器的呢?例如,咱们如今绑定了 3 个 URL,//hello/hello/world

显然,若是请求的 URL 为/,则调用/对应的处理器。若是请求的 URL 为/hello,则调用/hello对应的处理器。若是请求的 URL 为/hello/world,则调用/hello/world对应的处理器。
可是,若是请求的是/hello/others,那么使用哪个处理器呢? 匹配遵循如下规则:

  • 首先,精确匹配。即查找是否有/hello/others对应的处理器。若是有,则查找结束。若是没有,执行下一步;
  • 将路径中最后一个部分去掉,再次查找。即查找/hello/对应的处理器。若是有,则查找结束。若是没有,继续执行这一步。即查找/对应的处理器。

这里有一个注意点,若是注册的 URL 不是以/结尾的,那么它只能精确匹配请求的 URL。反之,即便请求的 URL 只有前缀与被绑定的 URL 相同,ServeMux也认为它们是匹配的。

这也是为何上面步骤进行到/hello/时,不能匹配/hello的缘由。由于/hello不以/结尾,必需要精确匹配。
若是,咱们绑定的 URL 为/hello/,那么当服务器找不到与/hello/others彻底匹配的处理器时,就会退而求其次,开始寻找可以与/hello/匹配的处理器。

看下面的代码:

package main

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

func indexHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "This is the index page")
}

func helloHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "This is the hello page")
}

func worldHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "This is the world page")
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", indexHandler)
    mux.HandleFunc("/hello", helloHandler)
    mux.HandleFunc("/hello/world", worldHandler)

    server := &http.Server{
        Addr:    ":8080",
        Handler: mux,
    }

    if err := server.ListenAndServe(); err != nil {
        log.Fatal(err)
    }
}
  • 浏览器请求localhost:8080/将返回"This is the index page",由于/精确匹配;

  • 浏览器请求localhost:8080/hello将返回"This is the hello page",由于/hello精确匹配;

  • 浏览器请求localhost:8080/hello/将返回"This is the index page"注意这里不是hello,由于绑定的/hello须要精确匹配,而请求的/hello/不能与之精确匹配。故而向上查找到/

  • 浏览器请求localhost:8080/hello/world将返回"This is the world page",由于/hello/world精确匹配;

  • 浏览器请求localhost:8080/hello/world/将返回"This is the index page"查找步骤为/hello/world/(不能与/hello/world精确匹配)-> /hello/(不能与/hello/精确匹配)-> /

  • 浏览器请求localhost:8080/hello/other将返回"This is the index page"查找步骤为/hello/others -> /hello/(不能与/hello精确匹配)-> /

若是注册时,将/hello改成/hello/,那么请求localhost:8080/hello/localhost:8080/hello/world/都将返回"This is the hello page"。本身试试吧!

思考:
使用/hello/注册处理器时,localhost:8080/hello/返回什么?

总结

本文介绍了 Go Web 程序的基本结构。Go Web 的基本形式以下:

package main

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

func helloHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello, World")
}

type greetingHandler struct {
    Name string
}

func (h greetingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello, %s", h.Name)
}

func main() {
    mux := http.NewServeMux()
    // 注册处理器函数
    mux.HandleFunc("/hello", helloHandler)
    
    // 注册处理器
    mux.Handle("/greeting/golang", greetingHandler{Name: "Golang"})
    
    server := &http.Server {
        Addr:       ":8080",
        Handler:    mux,
    }
    if err := server.ListenAndServe(); err != nil {
        log.Fatal(err)
    }
}

后续文章中大部分程序只是在此基础上增长处理器或处理器函数并注册到相应的 URL 中而已。处理器和处理器函数能够只使用一种或二者都使用。注意,为了方便,命名中我都加上了Handler

参考资料

  1. Go Web 编程
相关文章
相关标签/搜索