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 編程
相關文章
相關標籤/搜索