用Go語言寫了7年HTTP服務以後【譯】

趁着元旦休假+春節,嘗試把2018年期間讓我受益的一些文章、問答,翻譯一下。
歡迎指正、討論,但願對你也有所幫助。
原文:How I write Go HTTP services after seven yearshtml

如下,開始正文
我從r59(1.0版本以前的版本)便開始使用Go,過去7年裏一直用Go來編寫API和HTTP服務。在Machine Box(譯者注:做者公司),寫各式各樣的API是個人主要工做。咱們是作機器學習的,機器學習自己又很複雜,我編寫的API就是爲了讓開發者更容易理解和接入機器學習。目前爲止,收到的反饋還都不錯。git

若是你還沒嘗試過Machine Box,請趕忙試一試,並給我一些反饋吧。github

多年以來,我寫服務端程序的方式發生了不少變化,我想把我編寫服務端程序的方式分享給你,但願能對你有所幫助。golang

server structjson

我寫的組件基本都包含一個相似這樣的server結構體:api

type server struct {
    db     *someDatabase
    router *someRouter
    email  EmailSender
}

routes.go閉包

在組件裏還有一個單獨的文件routes.go,用來配置路由:app

package app
func (s *server) routes() {
    s.router.HandleFunc("/api/", s.handleAPI())
    s.router.HandleFunc("/about", s.handleAbout())
    s.router.HandleFunc("/", s.handleIndex())
}

routes.go很方便,由於維護代碼的時候大部分都從URL和錯誤日誌入手,看一眼routers.go,能幫咱們快速定位。框架

定義handler來處理不一樣請求機器學習

func (s *server) handleSomething() http.HandlerFunc { ... }

handler能夠經過s訪問相關數據。

返回handler
其實handler中並不直接處理請求,而是返回一個函數,創造一個閉包環境,在handler中咱們就能這樣操做了:

func (s *server) handleSomething() http.HandlerFunc {
    thing := prepareThing()
    return func(w http.ResponseWriter, r *http.Request) {
        // use thing        
    }
}

prepareThing只需調用一次,也就是你能夠經過在handler初始化時,只獲取一次thing變量,就能在整個handler中使用。但要保證獲取的是共享數據。若是handler中更改數據,須要使用mutex或者其餘方式加鎖保護。

經過傳參解決handler的特殊狀況
若是某個handler依賴外部數據,經過傳參來解決:

func (s *server) handleGreeting(format string) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, format, "World")
    }
}

format參數能夠被handler直接使用。

用HandlerFunc替換Handler
我如今在幾乎全部地方都用http.HandlerFunc來替換http.Handler了。

func (s *server) handleSomething() http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        ...
    }
}

這兩個類型不少狀況下均可以互換,對我來說http.HandlerFunc更易讀。

用Go函數實現中間件
中間件函數的入參是http.HandlerFunc,返回值是一個新http.HandlerFunc。新http.HandlerFunc能夠在原始HandlerFunc以前或者以後調用,甚至能夠決定不調用原始HandlerFunc(譯者注:看例子吧).

func (s *server) adminOnly(h http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        if !currentUser(r).IsAdmin {
            http.NotFound(w, r)
            return
        }
        h(w, r)
    }
}

中間件能夠選擇是否調用原始handler。以上面代碼爲例,若是IsAdmin爲false,中間件直接返回404,再也不調用h(w, r);若是IsAdmin爲true,h這個handler就被調用(h是傳入的參數)。
我一般在routers.go中列出中間件:

package app
func (s *server) routes() {
    s.router.HandleFunc("/api/", s.handleAPI())
    s.router.HandleFunc("/about", s.handleAbout())
    s.router.HandleFunc("/", s.handleIndex())
    s.router.HandleFunc("/admin", s.adminOnly(s.handleAdminIndex()))
}

特殊的請求類型和響應類型也能夠這樣處理
你要處理的特殊的請求類型和響應類型,通常也都是針對個別handler的。若是是這樣,你能夠在函數中直接定義使用:

func (s *server) handleSomething() http.HandlerFunc {
    type request struct {
        Name string
    }
    type response struct {
        Greeting string `json:"greeting"`
    }
    return func(w http.ResponseWriter, r *http.Request) {
        ...
    }
}

這樣作可讓代碼看起來更整潔,也容許你用相同名稱命名這些結構體。測試時,拷貝到測試函數中便可。
或者……

建立臨時測試類型讓測試更簡單
若是request或者response類型的定義隱藏在handler中,你能夠在測試代碼中聲明新類型完成測試。這也是一個闡明代碼歷史和設計的機會,能讓維護者更容易理解代碼。

舉例來說,咱們有一個Person類型,在不少接口中都要使用。若是咱們有個/greet接口,這個接口只關心Person類型的name字段,那咱們就能夠這樣來寫測試用例:

func TestGreet(t *testing.T) {
    is := is.New(t)
    p := struct {
        Name string `json:"name"`
    }{
        Name: "Mat Ryer",
    }
    var buf bytes.Buffer
    err := json.NewEncoder(&buf).Encode(p)
    is.NoErr(err) // json.NewEncoder
    req, err := http.NewRequest(http.MethodPost, "/greet", &buf)
    is.NoErr(err)
    //... more test code here

這段測試代碼很明顯地說明了Name字段纔是惟一須要關注的。

使用sync.Once

若是在預處理handler時必需要作一些耗資源的邏輯,我會把它推遲到第一次調用時處理。這麼處理能讓應用啓動更迅速。

func (s *server) handleTemplate(files string...) http.HandlerFunc {
    var (
        init sync.Once
        tpl  *template.Template
        err  error
    )
    return func(w http.ResponseWriter, r *http.Request) {
        init.Do(func(){
            tpl, err = template.ParseFiles(files...)
        })
        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }
        // use tpl
    }
}

sync.Once確保只執行一次,其餘請求在該邏輯處理完以前都會阻塞。

  • 爲了能在出錯時捕獲和保證日誌的完整,錯誤檢查放在了init 以外;
  • 若是handler沒被調用,耗資源邏輯永遠不會執行——這樣作好處很是明顯,固然也取決於代碼部署方式。

不過我要聲明,這樣處理是將初始化啓動時推遲到了運行時(首次訪問)。由於我常用 Google App Engine,對我而言這樣作優點明顯。但你可能面臨不一樣狀況,要因地制宜地考慮如何使用sync.Once

server類型方便測試
咱們的server類型很是便於測試。

func TestHandleAbout(t *testing.T) {
    is := is.New(t)
    srv := server{
        db:    mockDatabase,
        email: mockEmailSender,
    }
    srv.routes()
    req, err := http.NewRequest("GET", "/about", nil)
    is.NoErr(err)
    w := httptest.NewRecorder()
    srv.ServeHTTP(w, req)
    is.Equal(w.StatusCode, http.StatusOK)
}
  • 每一個測試用例建立一個server實例——耗資源能夠延遲加載,即便對大型組件總歸也浪費不了多少時間;
  • 調用srv.ServeHTTP時實際上是在測試整個調用棧了,也包括路由、中間件等等。若是想避免所有都調用,你也能夠直接調用對應的handler;
  • httptest.NewRecorder記錄handler都幹了啥;
  • 這段代碼用了我開發的一個小測試框架

總結
我但願文章內容對你有幫助,若是不一樣意本文觀點或者有其餘想法都歡迎在Twitter上和我討論。

相關文章
相關標籤/搜索