Go Web 編程之 程序結構


概述

一個典型的 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 編程

我的博客

歡迎關注我的微信公眾號【GoUpUp】,共同學習,一起進步~

本文由博客一文多發平台 OpenWrite 發布!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM