Go Web 编程之 程序结构

darjun · · 562 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

概述

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

  • 客户端发送请求;
  • 服务器中的多路复用器收到请求;
  • 多路复用器根据请求的 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时,会创建一个默认的服务器:

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

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

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 查找我们注册的处理器,然后将请求交由它处理。

虽然默认的多路复用器使用起来很方便,但是在生产环境中不建议使用。由于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 编程

有疑问加站长微信联系(非本文作者)

本文来自:Segmentfault

感谢作者:darjun

查看原文:Go Web 编程之 程序结构

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

562 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传