Go Web 服務

一個 Web 服務器

使用 Go 的庫非常容易實現一個 Web 服務器。

請求的 URL 路徑

這是一個迷你服務器,返回訪問服務器的 URL 的路徑部分。例如,如果請求的 URL 是 http://localhost:8000/hello,響應將是 URL.Path= "/hello"
下面是完整程序的程序:

// 迷你回聲服務器
package main

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

func main() {
    fmt.Println("http://localhost:8000/hello")
    http.HandleFunc("/", handler) // 回聲請求調用處理程序
    log.Fatal(http.ListenAndServe("localhost:8000", nil))
}

// 處理非持續回顯請求 URL r 的路徑部分
func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path)
}

請求的 URL 的路徑就是 r.URL.Path

多個處理函數

爲服務器添加功能很容易。一個有用的擴展是一個特定的 URL,下面的版本對 /count 請求會有特殊的響應:

// 迷你回聲和計數器服務器
package main

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

var mu sync.Mutex
var count int

func main() {
    fmt.Println("http://localhost:8000/hello")
    http.HandleFunc("/", handler)
    fmt.Println("http://localhost:8000/count")
    http.HandleFunc("/count", counter)
    log.Fatal(http.ListenAndServe("localhost:8000", nil))
}

// 處理程序回顯請求的 URL 的路徑部分
func handler(w http.ResponseWriter, r *http.Request) {
    mu.Lock()
    count++
    fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path)
    mu.Unlock()
}

// 回顯目前爲止調用的次數
func counter(w http.ResponseWriter, r *http.Request) {
    mu.Lock()
    fmt.Fprintf(w, "Count %d\n", count)
    mu.Unlock()
}

這個服務器有兩個處理函數,通過請求的 URL 來決定哪一個被調用。

請求頭和表單信息

下面這個示例中的處理函數,報告它接收到的請求頭和表單數據,這樣還方便服務器審查和調試請求:

package main

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

func main() {
    fmt.Println("http://localhost:8000/?k1=v1&k2=v2&k3=1&k3=2&k3=3")
    http.HandleFunc("/", handler)
    log.Fatal(http.ListenAndServe("localhost:8000", nil))
}

// 處理程序回顯 HTTP 請求
func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "%s %s %s\n", r.Method, r.URL, r.Proto)
    for k, v := range r.Header {
        fmt.Fprintf(w, "Header[%q] = %q\n", k, v)
    }
    fmt.Fprintf(w, "Host = %q\n", r.Host)
    fmt.Fprintf(w, "RemoteAddr = %q\n", r.RemoteAddr)
    if err := r.ParseForm(); err != nil {
        log.Print(err)
    }
    for k, v := range r.Form {
        fmt.Fprintf(w, "Form[%q] = %q\n", k, v)
    }
}

這裏彙報了很多的內容:

  • 請求方法 : r.Method
  • 請求路徑 : r.URL,這裏就是 r.URL.Path。r.URL是個結構體,這裏應該只有 Path 字段有內容。然後 %s 是調用它的 String 方法輸出
  • 請求協議 : r.Proto
  • 請求頭 : r.Header,這是個 map,這裏一項一項輸出了
  • 服務端地址 : r.Host,包括主機名和端口號
  • 客戶端地址 : r.RemoteAddr,包括主機名和端口號
  • 表單信息 : r.Form,這個先要用 r.ParseForm() 進行解析後纔會有內容。包括 Get 請求和 Post 請求的信息都會在 r.Form 這個 map 裏。

http.Handler 接口

進一步瞭解基於 http.Handler 接口的服務器API。

接口

下面是源碼中接口的定義:

package http

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

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

ListenAndServe 函數,這裏關注接口,只看函數的簽名,忽略函數體的內容。函數的第二個參數接收一個 Handler 接口的實例(用來接受所有的請求)。這個函數會一直執行,直到服務出錯時返回一個非空的錯誤值。

簡單的示例

下面的程序展示一個簡單的例子。使用map類型的database變量記錄商品和價格的映射。再加上一個 ServeHTTP 方法來滿足 http.Handler 接口。這個函數遍歷整個 map 並且輸出其中的元素:

package main

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

type dollars float32

func (d dollars) String() string { return fmt.Sprintf("$%.2f", d) }

type database map[string]dollars

func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    for item, price := range db {
        fmt.Fprintf(w, "%s: %s\n", item, price)
    }
}

func main() {
    db := database{"shoes": 50, "socks": 5}
    fmt.Println("http://localhost:8000")
    log.Fatal(http.ListenAndServe("localhost:8000", db))
}

添加功能

上面的示例中,服務器只能列出所有的商品,並且完全不管 URL,對每個請求都是同樣的功能。一般的 Web 服務會定義過個不同的 URL,每個觸發不同的行爲。把現有的功能的 URL 設置爲 /list,再加上另一個 /price 用來顯示單個商品的價格,商品可以在請求參數中指定,比如:/price?item=socks

package main

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

type dollars float32

func (d dollars) String() string { return fmt.Sprintf("$%.2f", d) }

type database map[string]dollars

func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    switch req.URL.Path {
    case "/list":
        for item, price := range db {
            fmt.Fprintf(w, "%s: %s\n", item, price)
        }
    case "/price":
        item := req.URL.Query().Get("item")
        price, ok := db[item]
        if !ok {
            w.WriteHeader(http.StatusNotFound) // 404
            fmt.Fprintf(w, "no such item: %q\n", item)
            // 也可以用 http.Error 實現上面2行的效果
            // http.Error(w, fmt.Sprintf("no such item: %q\n", item), http.StatusNotFound)
            return
        }
        fmt.Fprintf(w, "%s\n", price)
    default:
        w.WriteHeader(http.StatusNotFound) // 404
        fmt.Fprintf(w, "no such page: %s\n", req.URL)
        // http.Error(w, fmt.Sprintf("no such page: %s\n", req.URL), http.StatusNotFound)
    }
}

func main() {
    db := database{"shoes": 50, "socks": 5}
    fmt.Println("http://localhost:8000/list")
    fmt.Println("http://localhost:8000/price?item=shoes")
    log.Fatal(http.ListenAndServe("localhost:8000", db))
}

現在,處理函數基於 URL 的路徑部分(req.URL.Path)來決定執行哪部分邏輯。

返回錯誤頁面 404
如果處理函數不能識別這個路徑,那麼它通過調用w.WriteHeader(http.StatusNotFound)來返回一個 HTTP 錯誤。這個調用必須在網 w 中寫入內容之前執行。這裏還可以使用 http.Error 這個工具函數了達到同樣的目的:

msg := fmt.Sprintf("no such item: %q\n", item)
http.Error(w, msg, http.StatusNotFound) // 404

Get請求參數
對應 /price 的場景,它調用了 URL 的 Query 方法,把 HTTP 的請求參數解析爲一個map,或者更精確來講,解析爲一個 multimap,由 net/url 包的 url.Values 類型實現。這裏的 url.Values 是一個 map 映射:

type Values map[string][]string

它的 value 是一個 字符串切片,這裏用了 Get 方法,只會提取切片的第一個值。如果是要提取某個 key 所有的值,簡單的通過 map 的 key 提取 value 應該就好了。

優化添加功能

如果要繼續給 ServeHTTP 方法添加功能,應當把每部分邏輯分到獨立的函數或方法。net/http 包提供了一個請求多工轉發器 ServeMux,用來簡化 URL 和處理程序之間的關聯。一個 ServeMux 把多個 http.Handler 組合成單個 http.Handler。在這裏,可以看到滿足同一個接口的多個類型是可以互相替代的,Web 服務器可以把請求分發到任意一個 http.Handlr,而不用管後面具體的類型。
對於更加複雜的應用,多個 ServeMux 會組合起來,用來處理更復雜的分發需求。Go 語言並不需要一個類似於 Python 的 Django 那樣的權威 Web 框架。因爲 Go 語言的標準庫提供的基礎單元足夠靈活,以至於那樣的框架通常不是必須的。進一步來了講,儘管框架在項目初期帶來很多便利,但框架帶來了額外複雜性,增加長時間維護的難度。不過這樣的Web框架也是有的,比如:beego。
將程序修改爲使用 ServeMux,用於將 /list、/prics 這樣的 URL 和對應的處理程序關聯起來,這些處理程序也已經拆分到不同的方法中。最後作爲主處理程序在 ListenAndServe 調用中使用這個 ServeMux:

package main

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

type dollars float32

func (d dollars) String() string { return fmt.Sprintf("$%.2f", d) }

type database map[string]dollars

func (db database) list(w http.ResponseWriter, req *http.Request) {
    for item, price := range db {
        fmt.Fprintf(w, "%s: %s\n", item, price)
    }
}

func (db database) price(w http.ResponseWriter, req *http.Request) {
    item := req.URL.Query().Get("item")
    price, ok := db[item]
    if !ok {
        http.Error(w, fmt.Sprintf("no such item: %q\n", item), http.StatusNotFound)
        return
    }
    fmt.Fprintf(w, "%s\n", price)
}

func main() {
    db := database{"shoes": 50, "socks": 5}
    fmt.Println("http://localhost:8000/list")
    fmt.Println("http://localhost:8000/price?item=shoes")

    mux := http.NewServeMux()
    mux.Handle("/list", http.HandlerFunc(db.list))
    mux.Handle("/price", http.HandlerFunc(db.price))
    log.Fatal(http.ListenAndServe("localhost:8000", mux))
}

註冊處理程序
先關注一下用於註冊程序的兩次 mux.Handle 調用。在第一個調用中,db.list是一個方法值,即如下類型的一個值:

func(w http.ResponseWriter, req *http.Request)

當調用 db.list 時,等價於以 db 爲接收者調用 database.list 方法。所以 db.list 是一個實現了處理功能的函數。然而他沒有接口所需的方法,所以它不滿足 http.Handler 接口,也不能直接傳給 mux.Handle。
表達式http.HandlerFunc(db.list)其實是一個類型轉換,而不是函數調用。注意,http.HandlerFunc 是一個類型,它有如下定義:

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

http.HandlerFunc 這個函數類型它有自己的 ServeHTTP 方法,因此它滿足接口。而 http.HandlerFunc 的函數簽名和 db.list 這個方法值的函數簽名是一樣的,因此也能夠進行類型轉換。
這個是 Go 語言接口機制的一個不常見的特性。它不僅是一個函數類型,還可以擁有自己的方法,它的 ServeHTTP 方法就是調用函數本身,所以 HandlerFunc 是一個讓函數值滿足接口的一個適配器(關於適配器,我會在下一遍單獨講)。在這個例子裏,函數和接口的唯一方法擁有同樣的簽名。這個小技巧讓 database 類型可以用不同的方式來滿足 http.Handler 接口,一次通過 list 方法,一次通過 price 方法。

簡化註冊處理

因爲這種註冊處理程序的方法太常見了,所以 ServeMux 引入了一個 HandleFunc 便捷方法來簡化調用,處理程序註冊部分的代碼可以簡化爲如下的形式:

// mux.Handle("/list", http.HandlerFunc(db.list))
mux.HandleFunc("/list", db.list)
// mux.Handle("/prics", http.HandlerFunc(db.price))
mux.HandleFunc("/price", db.price)

全局 ServeMux 實例
通過 ServeMux,如果需要有兩個不同的 Web 服務,在不同的端口監聽。那麼就定義不同的 URL,分發到不同的處理程序。只須簡單地構造兩個 ServeMux,再調用一次 ListenAndServe 即可(建議併發調用)。不過很多時候一個 Web 服務足夠了,另外也不需要多個 ServeMux 實例。對於這種簡單的應用場景,建議用下面的簡化的調用方法。
net/http 包還提供了一個全局的 ServeMux 實例 DefaultServeMux,以及包級別的註冊函數 http.Handle 和 http.HandleFunc。要讓 DefaultServeMux 作爲服務器的主處理程序,無須把它傳給 ListenAndServe,直接傳nil即可。文章開頭的例子裏就是這麼用的。
服務器的主函數可以進一步簡化:

func main() {
    db := database{"shoes": 50, "socks": 5}
    http.HandleFunc("/list", db.list)
    http.HandleFunc("/price", db.price)
    log.Fatal(http.ListenAndServe("localhost:8000", nil))
}

併發安全問題

Web 服務器每次都用一個新的 goroutine 來調用處理程序,所以處理程序必須要注意併發問題。比如在訪問變量時的鎖問題,這個變量可能會被其他 goroutine 訪問,包括由同一個處理程序出廠的其他請求。文章開頭的第二個例子就要類似的處理。
併發安全是另外一塊內容,需要單獨研究和解決,這裏去簡單提一下。如果要添加創建、更新商品的功能,就需要注意併發安全。

功能需求
增加額外的處理程序,來支持創建、讀取、更新和刪除數據庫條目。比如,/update?item=socke&price=6這樣的請求將更新倉庫中物品的價格,如果商品不存在或者價格無效就返回錯誤。(注意:這次修改會引入併發變量修改。)
Go 語言有兩種實現併發安全的方式,這裏通過加鎖來保證併發安全:

package main

import (
    "errors"
    "fmt"
    "log"
    "net/http"
    "strconv"
    "sync"
)

type dollars float32

func (d dollars) String() string { return fmt.Sprintf("$%.2f", d) }

type database struct {
    items map[string]dollars
    sync.RWMutex
}

func (db *database) list(w http.ResponseWriter, req *http.Request) {
    db.RLock()
    defer db.RUnlock()
    for item, price := range db.items {
        fmt.Fprintf(w, "%s: %s\n", item, price)
    }
}

func (db *database) price(w http.ResponseWriter, req *http.Request) {
    item := req.URL.Query().Get("item")
    db.RLock()
    defer db.RUnlock()
    price, ok := db.items[item]
    if !ok {
        http.Error(w, fmt.Sprintf("no such item: %q\n", item), http.StatusNotFound)
        return
    }
    fmt.Fprintf(w, "%s\n", price)
}

// 從 URL 解析獲取item和price
func getItemPrice(req *http.Request) (string, dollars, error) {
    item := req.URL.Query().Get("item")
    if item == "" {
        return "", 0, errors.New("item not get")
    }
    priceStr := req.URL.Query().Get("price")
    if priceStr == "" {
        return item, 0, errors.New("price not get")
    }
    price64, err := strconv.ParseFloat(priceStr, 32)
    price := dollars(price64)
    if err != nil {
        return item, price, fmt.Errorf("Parse Price: %v\n", err)
    }
    return item, price, err
}

func (db *database) add(w http.ResponseWriter, req *http.Request) {
    item, price, err := getItemPrice(req)
    if err != nil {
        http.Error(w, fmt.Sprintln(err), http.StatusNotFound)
        return
    }
    db.Lock()
    defer db.Unlock()
    if _, ok := db.items[item]; ok {
        http.Error(w, fmt.Sprintf("%s is already exist.\n", item), http.StatusNotFound)
        return
    }
    db.items[item] = dollars(price)
    fmt.Fprintf(w, "success add %s: %s\n", item, dollars(price))
}

func (db *database) update(w http.ResponseWriter, req *http.Request) {
    item, price, err := getItemPrice(req)
    if err != nil {
        http.Error(w, fmt.Sprintln(err), http.StatusNotFound)
        return
    }
    db.Lock()
    defer db.Unlock()
    if _, ok := db.items[item]; !ok {
        http.Error(w, fmt.Sprintf("%s is not exist.\n", item), http.StatusNotFound)
        return
    }
    db.items[item] = dollars(price)
    fmt.Fprintf(w, "success udate %s: %s\n", item, dollars(price))
}

func (db *database) delete(w http.ResponseWriter, req *http.Request) {
    item := req.URL.Query().Get("item")
    func () {
        db.Lock()
        defer db.Unlock()
        delete(db.items, item)
    }()
    db.list(w, req)
}

func main() {
    db := database{
        items: map[string]dollars{"shoes": 50, "socks": 5},
    }
    fmt.Println("http://localhost:8000/list")
    fmt.Println("http://localhost:8000/price?item=shoes")
    fmt.Println("http://localhost:8000/add?item=football&price=11")
    fmt.Println("http://localhost:8000/update?item=football&price=12.35")
    fmt.Println("http://localhost:8000/delete?item=shoes")
    http.HandleFunc("/list", db.list)
    http.HandleFunc("/price", db.price)
    http.HandleFunc("/add", db.add)
    http.HandleFunc("/update", db.update)
    http.HandleFunc("/delete", db.delete)
    log.Fatal(http.ListenAndServe("localhost:8000", nil))
}

不但新增的創建、更新和刪除的方法要加鎖,因爲現在有了併發安全問題,原本的讀取方法也需要加鎖,才能保證讀取到的數據是當前最新的。

部署

這部分內容是從別處收集來了。

反向代理

Go 語言原生支持 http,所有 Go 的http服務性能和nginx比較接近。如果用 Go 寫的 Web 程序上線,程序前面不需要再部署nginx的Web服務器,這樣就省掉的是Web服務器。這是單應用的部署。
對於多應用部署,服務器需要部署多個Web應用,這時就需要反向代理了,一般這也是nginx或apache。
反向代理,有個很棒的說法是流量轉發。我獲取到客戶端來的請求,將它發往另一個服務器,從服務器獲取到響應再回給原先的客戶端。反向的意義簡單來說在於這個代理自身決定了何時將流量發往何處。
Go 的反向代理,可以參考下這篇。1 行 Go 代碼實現反向代理:
https://studygolang.com/articles/14246

Panic 處理

下面是我之前寫的另一篇有個 HTTP 服務端內容的,主要是這篇裏的Panic 處理這個小章節,讓程序可以在處理函數發生崩潰之後可以通過 revoer 來自動恢復:
https://blog.51cto.com/steed/2321827

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章