golang http 伺服器程式設計

cizixs發表於2017-03-20

1. 初識

http 是典型的 C/S 架構,客戶端向服務端傳送請求(request),服務端做出應答(response)。

golang 的標準庫 net/http 提供了 http 程式設計有關的介面,封裝了內部TCP連線和報文解析的複雜瑣碎的細節,使用者只需要和 http.requesthttp.ResponseWriter 兩個物件互動就行。也就是說,我們只要寫一個 handler,請求會通過引數傳遞進來,而它要做的就是根據請求的資料做處理,把結果寫到 Response 中。廢話不多說,來看看 hello world 程式有多簡單吧!

package main

import (
    "io"
    "net/http"
)

type helloHandler struct{}

func (h *helloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Hello, world!"))
}

func main() {
    http.Handle("/", &helloHandler{})
    http.ListenAndServe(":12345", nil)
}複製程式碼

執行 go run hello_server.go,我們的伺服器就會監聽在本地的 12345 埠,對所有的請求都會返回 hello, world!

golang http 伺服器程式設計

正如上面程式展示的那樣,我們只要實現的一個 Handler,它的介面原型是(也就是說只要實現了 ServeHTTP 方法的物件都可以作為 Handler):

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}複製程式碼

然後,註冊到對應的路由路徑上就 OK 了。

http.HandleFunc接受兩個引數:第一個引數是字串表示的 url 路徑,第二個引數是該 url 實際的處理物件。

http.ListenAndServe 監聽在某個埠,啟動服務,準備接受客戶端的請求(第二個引數這裡設定為 nil,這裡也不要糾結什麼意思,後面會有講解)。每次客戶端有請求的時候,把請求封裝成 http.Request,呼叫對應的 handler 的 ServeHTTP 方法,然後把操作後的 http.ResponseWriter 解析,返回到客戶端。

2. 封裝

上面的程式碼沒有什麼問題,但是有一個不便:每次寫 Handler 的時候,都要定義一個型別,然後編寫對應的 ServeHTTP 方法,這個步驟對於所有 Handler 都是一樣的。重複的工作總是可以抽象出來,net/http 也正這麼做了,它提供了 http.HandleFunc 方法,允許直接把特定型別的函式作為 handler。上面的程式碼可以改成:

package main

import (
    "io"
    "net/http"
)

func helloHandler(w http.ResponseWriter, req *http.Request) {
    io.WriteString(w, "hello, world!\n")
}

func main() {
    http.HandleFunc("/", helloHandler)
    http.ListenAndServe(":12345", nil)
}複製程式碼

其實,HandleFunc 只是一個介面卡,

// The HandlerFunc type is an adapter to allow the use of
// ordinary functions as HTTP handlers.  If f is a function
// with the appropriate signature, HandlerFunc(f) is a
// Handler object that calls f.
type HandlerFunc func(ResponseWriter, *Request)

// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}複製程式碼

自動給 f 函式新增了 HandlerFunc 這個殼,最終呼叫的還是 ServerHTTP,只不過會直接使用 f(w, r)。這樣封裝的好處是:使用者可以專注於業務邏輯的編寫,省去了很多重複的程式碼處理邏輯。如果只是簡單的 Handler,會直接使用函式;如果是需要傳遞更多資訊或者有複雜的操作,會使用上部分的方法。

如果需要我們自己寫的話,是這樣的:

package main

import (
    "io"
    "net/http"
)

func helloHandler(w http.ResponseWriter, req *http.Request) {
    io.WriteString(w, "hello, world!\n")
}

func main() {
    // 通過 HandlerFunc 把函式轉換成 Handler 介面的實現物件
    hh := http.HandlerFunc(helloHandler)
    http.Handle("/", hh)
    http.ListenAndServe(":12345", nil)
}複製程式碼

3. 預設

大部分的伺服器邏輯都需要使用者編寫對應的 Handler,不過有些 Handler 使用頻繁,因此 net/http 提供了它們的實現。比如負責檔案 hosting 的 FileServer、負責 404 的NotFoundHandler 和 負責重定向的RedirectHandler。下面這個簡單的例子,把當前目錄所有檔案 host 到服務端:

package main

import (
    "net/http"
)

func main() {
    http.ListenAndServe(":12345", http.FileServer(http.Dir(".")))
}複製程式碼

強大吧!只要一行邏輯程式碼就能實現一個簡單的靜態檔案伺服器。從這裡可以看出一件事:http.ListenAndServe 第二個引數就是一個 Handler 函式(請記住這一點,後面有些內容依賴於這個)。

執行這個程式,在瀏覽器中開啟 http://127.0.0.1:12345,可以看到所有的檔案,點選對應的檔案還能看到它的內容。

golang http 伺服器程式設計

其他兩個 Handler,這裡就不再舉例子了,讀者可以自行參考文件。

4. 路由

雖然上面的程式碼已經工作,並且能實現很多功能,但是實際開發中,HTTP 介面會有許多的 URL 和對應的 Handler。這裡就要講 net/http 的另外一個重要的概念:ServeMuxMuxmultiplexor 的縮寫,就是多路傳輸的意思(請求傳過來,根據某種判斷,分流到後端多個不同的地方)。ServeMux 可以註冊多了 URL 和 handler 的對應關係,並自動把請求轉發到對應的 handler 進行處理。我們還是來看例子吧:

package main

import (
    "io"
    "net/http"
)

func helloHandler(w http.ResponseWriter, r *http.Request) {
    io.WriteString(w, "Hello, world!\n")
}

func echoHandler(w http.ResponseWriter, r *http.Request) {
    io.WriteString(w, r.URL.Path)
}

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

    http.ListenAndServe(":12345", mux)
}複製程式碼

這個伺服器的功能也很簡單:如果在請求的 URL 是 /hello,就返回 hello, world!;否則就返回 URL 的路徑,路徑是從請求物件 http.Requests 中提取的。

golang http 伺服器程式設計

這段程式碼和之前的程式碼有兩點區別:

  1. 通過 NewServeMux 生成了 ServerMux 結構,URL 和 handler 是通過它註冊的
  2. http.ListenAndServe 方法第二個引數變成了上面的 mux 變數

還記得我們之前說過,http.ListenAndServe 第二個引數應該是 Handler 型別的變數嗎?這裡為什麼能傳過來 ServeMux?嗯,估計你也猜到啦:ServeMux 也是是 Handler 介面的實現,也就是說它實現了 ServeHTTP 方法,我們來看一下:

type ServeMux struct {
        // contains filtered or unexported fields
}

func NewServeMux() *ServeMux
func (mux *ServeMux) Handle(pattern string, handler Handler)
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request))
func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string)
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request)複製程式碼

哈!果然,這裡的方法我們大都很熟悉,除了 Handler() 返回某個請求的 Handler。HandleHandleFunc 這兩個方法 net/http 也提供了,後面我們會說明它們之間的關係。而 ServeHTTP 就是 ServeMux 的核心處理邏輯:根據傳遞過來的 Request,匹配之前註冊的 URL 和處理函式,找到最匹配的項,進行處理。可以說 ServeMux 是個特殊的 Handler,它負責路由和呼叫其他後端 Handler 的處理方法。

關於ServeMux ,有幾點要說明:

  • URL 分為兩種,末尾是 /:表示一個子樹,後面可以跟其他子路徑; 末尾不是 /,表示一個葉子,固定的路徑
  • / 結尾的 URL 可以匹配它的任何子路徑,比如 /images 會匹配 /images/cute-cat.jpg
  • 它採用最長匹配原則,如果有多個匹配,一定採用匹配路徑最長的那個進行處理
  • 如果沒有找到任何匹配項,會返回 404 錯誤
  • ServeMux 也會識別和處理 ...,正確轉換成對應的 URL 地址

你可能會有疑問?我們之間為什麼沒有使用 ServeMux 就能實現路徑功能?那是因為 net/http 在後臺預設建立使用了 DefaultServeMux

5. 深入

嗯,上面基本覆蓋了編寫 HTTP 服務端需要的所有內容。這部分就分析一下,它們的原始碼實現,加深理解,以後遇到疑惑也能通過原始碼來定位和解決。

Server

首先來看 http.ListenAndServe():

func ListenAndServe(addr string, handler Handler) error {
    server := &Server{Addr: addr, Handler: handler}
    return server.ListenAndServe()
}複製程式碼

這個函式其實也是一層封裝,建立了 Server 結構,並呼叫它的 ListenAndServe 方法,那我們就跟進去看看:

// A Server defines parameters for running an HTTP server.
// The zero value for Server is a valid configuration.
type Server struct {
    Addr           string        // TCP address to listen on, ":http" if empty
    Handler        Handler       // handler to invoke, http.DefaultServeMux if nil
    ......
}

// ListenAndServe listens on the TCP network address srv.Addr and then
// calls Serve to handle requests on incoming connections.  If
// srv.Addr is blank, ":http" is used.
func (srv *Server) ListenAndServe() error {
    addr := srv.Addr
    if addr == "" {
        addr = ":http"
    }
    ln, err := net.Listen("tcp", addr)
    if err != nil {
        return err
    }
    return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})
}複製程式碼

Server 儲存了執行 HTTP 服務需要的引數,呼叫 net.Listen 監聽在對應的 tcp 埠,tcpKeepAliveListener 設定了 TCP 的 KeepAlive 功能,最後呼叫 srv.Serve()方法開始真正的迴圈邏輯。我們再跟進去看看 Serve 方法:

// Serve accepts incoming connections on the Listener l, creating a
// new service goroutine for each.  The service goroutines read requests and
// then call srv.Handler to reply to them.
func (srv *Server) Serve(l net.Listener) error {
    defer l.Close()
    var tempDelay time.Duration // how long to sleep on accept failure
    // 迴圈邏輯,接受請求並處理
    for {
         // 有新的連線
        rw, e := l.Accept()
        if e != nil {
            if ne, ok := e.(net.Error); ok && ne.Temporary() {
                if tempDelay == 0 {
                    tempDelay = 5 * time.Millisecond
                } else {
                    tempDelay *= 2
                }
                if max := 1 * time.Second; tempDelay > max {
                    tempDelay = max
                }
                srv.logf("http: Accept error: %v; retrying in %v", e, tempDelay)
                time.Sleep(tempDelay)
                continue
            }
            return e
        }
        tempDelay = 0
         // 建立 Conn 連線
        c, err := srv.newConn(rw)
        if err != nil {
            continue
        }
        c.setState(c.rwc, StateNew) // before Serve can return
         // 啟動新的 goroutine 進行處理
        go c.serve()
    }
}複製程式碼

最上面的註釋也說明了這個方法的主要功能:

  • 接受 Listener l 傳遞過來的請求
  • 為每個請求建立 goroutine 進行後臺處理
  • goroutine 會讀取請求,呼叫 srv.Handler
func (c *conn) serve() {
    origConn := c.rwc // copy it before it's set nil on Close or Hijack

      ...

    for {
        w, err := c.readRequest()
        if c.lr.N != c.server.initialLimitedReaderSize() {
            // If we read any bytes off the wire, we're active.
            c.setState(c.rwc, StateActive)
        }

         ...

        // HTTP cannot have multiple simultaneous active requests.[*]
        // Until the server replies to this request, it can't read another,
        // so we might as well run the handler in this goroutine.
        // [*] Not strictly true: HTTP pipelining.  We could let them all process
        // in parallel even if their responses need to be serialized.
        serverHandler{c.server}.ServeHTTP(w, w.req)

        w.finishRequest()
        if w.closeAfterReply {
            if w.requestBodyLimitHit {
                c.closeWriteAndWait()
            }
            break
        }
        c.setState(c.rwc, StateIdle)
    }
}複製程式碼

看到上面這段程式碼 serverHandler{c.server}.ServeHTTP(w, w.req)這一句了嗎?它會呼叫最早傳遞給 Server 的 Handler 函式:

func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
    handler := sh.srv.Handler
    if handler == nil {
        handler = DefaultServeMux
    }
    if req.RequestURI == "*" && req.Method == "OPTIONS" {
        handler = globalOptionsHandler{}
    }
    handler.ServeHTTP(rw, req)
}複製程式碼

哇!這裡看到 DefaultServeMux 了嗎?如果沒有 handler 為空,就會使用它。handler.ServeHTTP(rw, req),Handler 介面都要實現 ServeHTTP 這個方法,因為這裡就要被呼叫啦。

也就是說,無論如何,最終都會用到 ServeMux,也就是負責 URL 路由的傢伙。前面也已經說過,它的 ServeHTTP 方法就是根據請求的路徑,把它轉交給註冊的 handler 進行處理。這次,我們就在原始碼層面一探究竟。

ServeMux

我們已經知道,ServeMux 會以某種方式儲存 URL 和 Handlers 的對應關係,下面我們就從程式碼層面來解開這個祕密:

type ServeMux struct {
    mu    sync.RWMutex
    m     map[string]muxEntry  // 存放路由資訊的字典!\(^o^)/
    hosts bool // whether any patterns contain hostnames
}

type muxEntry struct {
    explicit bool
    h        Handler
    pattern  string
}複製程式碼

沒錯,資料結構也比較直觀,和我們想象的差不多,路由資訊儲存在字典中,接下來就看看幾個重要的操作:路由資訊是怎麼註冊的?ServeHTTP 方法到底是怎麼做的?路由查詢過程是怎樣的?

// Handle registers the handler for the given pattern.
// If a handler already exists for pattern, Handle panics.
func (mux *ServeMux) Handle(pattern string, handler Handler) {
    mux.mu.Lock()
    defer mux.mu.Unlock()

    // 邊界情況處理
    if pattern == "" {
        panic("http: invalid pattern " + pattern)
    }
    if handler == nil {
        panic("http: nil handler")
    }
    if mux.m[pattern].explicit {
        panic("http: multiple registrations for " + pattern)
    }

    // 建立 `muxEntry` 並新增到路由字典中
    mux.m[pattern] = muxEntry{explicit: true, h: handler, pattern: pattern}

    if pattern[0] != '/' {
        mux.hosts = true
    }

    // 這是一個很有用的小技巧,如果註冊了 `/tree/`, `serveMux` 會自動新增一個 `/tree` 的路徑並重定向到 `/tree/`。當然這個 `/tree` 路徑會被使用者顯示的路由資訊覆蓋。
    // Helpful behavior:
    // If pattern is /tree/, insert an implicit permanent redirect for /tree.
    // It can be overridden by an explicit registration.
    n := len(pattern)
    if n > 0 && pattern[n-1] == '/' && !mux.m[pattern[0:n-1]].explicit {
        // If pattern contains a host name, strip it and use remaining
        // path for redirect.
        path := pattern
        if pattern[0] != '/' {
            // In pattern, at least the last character is a '/', so
            // strings.Index can't be -1.
            path = pattern[strings.Index(pattern, "/"):]
        }
        mux.m[pattern[0:n-1]] = muxEntry{h: RedirectHandler(path, StatusMovedPermanently), pattern: pattern}
    }
}複製程式碼

路由註冊沒有什麼特殊的地方,很簡單,也符合我們的預期,注意最後一段程式碼對類似 /tree URL 重定向的處理。

// ServeHTTP dispatches the request to the handler whose
// pattern most closely matches the request URL.
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
    if r.RequestURI == "*" {
        if r.ProtoAtLeast(1, 1) {
            w.Header().Set("Connection", "close")
        }
        w.WriteHeader(StatusBadRequest)
        return
    }
    h, _ := mux.Handler(r)
    h.ServeHTTP(w, r)
}複製程式碼

好吧,ServeHTTP 也只是通過 mux.Handler(r) 找到請求對應的 handler,呼叫它的 ServeHTTP 方法,程式碼比較簡單我們就顯示了,它最終會呼叫 mux.match() 方法,我們來看一下它的實現:

// Does path match pattern?
func pathMatch(pattern, path string) bool {
    if len(pattern) == 0 {
        // should not happen
        return false
    }
    n := len(pattern)
    if pattern[n-1] != '/' {
        return pattern == path
    }
    // 匹配的邏輯很簡單,path 前面的字元和 pattern 一樣就是匹配
    return len(path) >= n && path[0:n] == pattern
}

// Find a handler on a handler map given a path string
// Most-specific (longest) pattern wins
func (mux *ServeMux) match(path string) (h Handler, pattern string) {
    var n = 0
    for k, v := range mux.m {
        if !pathMatch(k, path) {
            continue
        }
         // 最長匹配的邏輯在這裡
        if h == nil || len(k) > n {
            n = len(k)
            h = v.h
            pattern = v.pattern
        }
    }
    return
}複製程式碼

match 會遍歷路由資訊字典,找到所有匹配該路徑最長的那個。路由部分的程式碼解釋就到這裡了,最後回答上面的一個問題:http.HandleFuncServeMux.HandlerFunc 是什麼關係?

// Handle registers the handler for the given pattern
// in the DefaultServeMux.
// The documentation for ServeMux explains how patterns are matched.
func Handle(pattern string, handler Handler) { DefaultServeMux.Handle(pattern, handler) }

// HandleFunc registers the handler function for the given pattern
// in the DefaultServeMux.
// The documentation for ServeMux explains how patterns are matched.
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    DefaultServeMux.HandleFunc(pattern, handler)
}複製程式碼

原來是直接通過 DefaultServeMux 呼叫對應的方法,到這裡上面的一切都串起來了!

Request

最後一部分,要講講 Handler 函式接受的兩個引數:http.Requesthttp.ResponseWriter

Request 就是封裝好的客戶端請求,包括 URL,method,header 等等所有資訊,以及一些方便使用的方法:

// A Request represents an HTTP request received by a server
// or to be sent by a client.
//
// The field semantics differ slightly between client and server
// usage. In addition to the notes on the fields below, see the
// documentation for Request.Write and RoundTripper.
type Request struct {
    // Method specifies the HTTP method (GET, POST, PUT, etc.).
    // For client requests an empty string means GET.
    Method string

    // URL specifies either the URI being requested (for server
    // requests) or the URL to access (for client requests).
    //
    // For server requests the URL is parsed from the URI
    // supplied on the Request-Line as stored in RequestURI.  For
    // most requests, fields other than Path and RawQuery will be
    // empty. (See RFC 2616, Section 5.1.2)
    //
    // For client requests, the URL's Host specifies the server to
    // connect to, while the Request's Host field optionally
    // specifies the Host header value to send in the HTTP
    // request.
    URL *url.URL

    // The protocol version for incoming requests.
    // Client requests always use HTTP/1.1.
    Proto      string // "HTTP/1.0"
    ProtoMajor int    // 1
    ProtoMinor int    // 0

    // A header maps request lines to their values.
    // If the header says
    //
    //    accept-encoding: gzip, deflate
    //    Accept-Language: en-us
    //    Connection: keep-alive
    //
    // then
    //
    //    Header = map[string][]string{
    //        "Accept-Encoding": {"gzip, deflate"},
    //        "Accept-Language": {"en-us"},
    //        "Connection": {"keep-alive"},
    //    }
    //
    // HTTP defines that header names are case-insensitive.
    // The request parser implements this by canonicalizing the
    // name, making the first character and any characters
    // following a hyphen uppercase and the rest lowercase.
    //
    // For client requests certain headers are automatically
    // added and may override values in Header.
    //
    // See the documentation for the Request.Write method.
    Header Header

    // Body is the request's body.
    //
    // For client requests a nil body means the request has no
    // body, such as a GET request. The HTTP Client's Transport
    // is responsible for calling the Close method.
    //
    // For server requests the Request Body is always non-nil
    // but will return EOF immediately when no body is present.
    // The Server will close the request body. The ServeHTTP
    // Handler does not need to.
    Body io.ReadCloser

    // ContentLength records the length of the associated content.
    // The value -1 indicates that the length is unknown.
    // Values >= 0 indicate that the given number of bytes may
    // be read from Body.
    // For client requests, a value of 0 means unknown if Body is not nil.
    ContentLength int64

    // TransferEncoding lists the transfer encodings from outermost to
    // innermost. An empty list denotes the "identity" encoding.
    // TransferEncoding can usually be ignored; chunked encoding is
    // automatically added and removed as necessary when sending and
    // receiving requests.
    TransferEncoding []string

    // Close indicates whether to close the connection after
    // replying to this request (for servers) or after sending
    // the request (for clients).
    Close bool

    // For server requests Host specifies the host on which the
    // URL is sought. Per RFC 2616, this is either the value of
    // the "Host" header or the host name given in the URL itself.
    // It may be of the form "host:port".
    //
    // For client requests Host optionally overrides the Host
    // header to send. If empty, the Request.Write method uses
    // the value of URL.Host.
    Host string

    // Form contains the parsed form data, including both the URL
    // field's query parameters and the POST or PUT form data.
    // This field is only available after ParseForm is called.
    // The HTTP client ignores Form and uses Body instead.
    Form url.Values

    // PostForm contains the parsed form data from POST or PUT
    // body parameters.
    // This field is only available after ParseForm is called.
    // The HTTP client ignores PostForm and uses Body instead.
    PostForm url.Values

    // MultipartForm is the parsed multipart form, including file uploads.
    // This field is only available after ParseMultipartForm is called.
    // The HTTP client ignores MultipartForm and uses Body instead.
    MultipartForm *multipart.Form

    ...

    // RemoteAddr allows HTTP servers and other software to record
    // the network address that sent the request, usually for
    // logging. This field is not filled in by ReadRequest and
    // has no defined format. The HTTP server in this package
    // sets RemoteAddr to an "IP:port" address before invoking a
    // handler.
    // This field is ignored by the HTTP client.
    RemoteAddr string
    ...
}複製程式碼

Handler 需要知道關於請求的任何資訊,都要從這個物件中獲取,一般不會直接修改這個物件(除非你非常清楚自己在做什麼)!

ResponseWriter

ResponseWriter 是一個介面,定義了三個方法:

  • Header():返回一個 Header 物件,可以通過它的 Set() 方法設定頭部,注意最終返回的頭部資訊可能和你寫進去的不完全相同,因為後續處理還可能修改頭部的值(比如設定 Content-LengthContent-type 等操作)
  • Write(): 寫 response 的主體部分,比如 html 或者 json 的內容就是放到這裡的
  • WriteHeader():設定 status code,如果沒有呼叫這個函式,預設設定為 http.StatusOK, 就是 200 狀態碼
// A ResponseWriter interface is used by an HTTP handler to
// construct an HTTP response.
type ResponseWriter interface {
    // Header returns the header map that will be sent by WriteHeader.
    // Changing the header after a call to WriteHeader (or Write) has
    // no effect.
    Header() Header

    // Write writes the data to the connection as part of an HTTP reply.
    // If WriteHeader has not yet been called, Write calls WriteHeader(http.StatusOK)
    // before writing the data.  If the Header does not contain a
    // Content-Type line, Write adds a Content-Type set to the result of passing
    // the initial 512 bytes of written data to DetectContentType.
    Write([]byte) (int, error)

    // WriteHeader sends an HTTP response header with status code.
    // If WriteHeader is not called explicitly, the first call to Write
    // will trigger an implicit WriteHeader(http.StatusOK).
    // Thus explicit calls to WriteHeader are mainly used to
    // send error codes.
    WriteHeader(int)
}複製程式碼

實際上傳遞給 Handler 的物件是:

// A response represents the server side of an HTTP response.
type response struct {
    conn          *conn
    req           *Request // request for this response
    wroteHeader   bool     // reply header has been (logically) written
    wroteContinue bool     // 100 Continue response was written

    w  *bufio.Writer // buffers output in chunks to chunkWriter
    cw chunkWriter
    sw *switchWriter // of the bufio.Writer, for return to putBufioWriter

    // handlerHeader is the Header that Handlers get access to,
    // which may be retained and mutated even after WriteHeader.
    // handlerHeader is copied into cw.header at WriteHeader
    // time, and privately mutated thereafter.
    handlerHeader Header
    ...
    status        int   // status code passed to WriteHeader
    ...
}複製程式碼

它當然實現了上面提到的三個方法,具體程式碼就不放到這裡了,感興趣的可以自己去看。

6. 擴充套件

雖然 net/http 提供的各種功能已經滿足基本需求了,但是很多時候還不夠方便,比如:

  • 不支援 URL 匹配,所有的路徑必須完全匹配,不能捕獲 URL 中的變數,不夠靈活
  • 不支援 HTTP 方法匹配
  • 不支援擴充套件和巢狀,URL 處理都在都一個 ServeMux 變數中

雖然這些都可以自己手動去碼,但實在很不方便。這部分看看有哪些三方的包,都提供了哪些額外的功能。

alice

alice 的功能很簡單——把多個 handler 串聯起來,有請求過來的時候,逐個通過這個 handler 進行處理。

alice.New(Middleware1, Middleware2, Middleware3).Then(App)複製程式碼

Gorilla Mux

Gorilla 提供了很多網路有關的元件, Mux 就是其中一個,負責 HTTP 的路由功能。這個元件彌補了上面提到的 ServeMux 的一些缺陷,支援的功能有:

  • 更多的匹配型別:HTTP 方法、query 欄位、URL host 等
  • 支援正規表示式作為 URL path 的一部分,也支援變數提取功能
  • 支援子路由,也就是路由的巢狀,SubRouter 可以實現路由資訊的傳遞
  • 並且和 ServeMux 完全相容
r := mux.NewRouter()
r.HandleFunc("/products/{key}", ProductHandler)
r.HandleFunc("/articles/{category}/", ArticlesCategoryHandler)
r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler)複製程式碼

httprouter

httprouter 和 mux 一樣,也是擴充套件了自帶 ServeMux 功能的路由庫。它的主要特點是速度快、記憶體使用少、可擴充套件性高(使用 radix tree 資料結構進行路由匹配,路由項很多的時候速度也很快)。

package main

import (
    "fmt"
    "github.com/julienschmidt/httprouter"
    "net/http"
    "log"
)

func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
    fmt.Fprint(w, "Welcome!\n")
}

func Hello(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
    fmt.Fprintf(w, "hello, %s!\n", ps.ByName("name"))
}

func main() {
    router := httprouter.New()
    router.GET("/", Index)
    router.GET("/hello/:name", Hello)

    log.Fatal(http.ListenAndServe(":8080", router))
}複製程式碼

negroni

http middleware 庫,支援巢狀的中介軟體,能夠和其他路由庫相容。同時它也自帶了不少 middleware 可以使用,比如RecoveryLoggerStatic

router := mux.NewRouter()
router.HandleFunc("/", HomeHandler)

n := negroni.New(Middleware1, Middleware2)
// Or use a middleware with the Use() function
n.Use(Middleware3)
// router goes last
n.UseHandler(router)

http.ListenAndServe(":3001", n)複製程式碼

7. 參考

這篇文章參考了以下資料:

相關文章