在使用Go七年后我如何編寫Go HTTP服務(wù)

原文:https://medium.com/statuscode/how-i-write-go-http-services-after-seven-years-37c208122831
翻譯:devabel
自從r59(一個1.0之前的版本)以來,我一直在寫Go(那時還不叫Golang),并且在過去的七年里一直在Go中構(gòu)建HTTP API和服務(wù)。
我在Machine Box工作,我的大多數(shù)技術(shù)工作都涉及構(gòu)建各種API。 機器學(xué)習(xí)很復(fù)雜,大多數(shù)開發(fā)人員都無法掌握,因此我的工作是通過API接口簡化這個過程,到目前為止我們已經(jīng)得到了很好的反饋。
如果您還沒有親眼嘗試過Machine Box開發(fā)者的體驗,請試一試,讓我知道您的想法。
多年來,我編寫服務(wù)的方式發(fā)生了變化,因此今天我想分享如何編寫服務(wù) - 也許這種方式對您的工作有幫助。

A server struct

我的所有組件都有一個 server結(jié)構(gòu)體,通??雌饋硐襁@樣:

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

共享依賴項是結(jié)構(gòu)的字段

routes.go

我的每個組件中都有一個名為routes.go的文件,其中所有路由都在這個文件里:

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

這很方便,因為大多數(shù)代碼維護(hù)都是以URL和錯誤報告開始的 - 所以只需瀏覽一下routes.go即可幫助我們調(diào)試。

處理程序掛起服務(wù)器

我的HTTP處理程序掛起了服務(wù)器:

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

處理程序可以通過s服務(wù)器變量訪問依賴項。

返回處理程序

我的處理函數(shù)實際上并不處理請求,它們返回一個函數(shù)。
這給了我們一個閉包環(huán)境,我們的處理程序可以在其中運行

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

prepareThing僅被調(diào)用一次,因此您可以使用它來執(zhí)行一次性每個處理程序初始化,然后在處理程序中使用該事物。
確保只讀取共享數(shù)據(jù),如果處理程序正在修改任何內(nèi)容,請記住您需要一個互斥鎖或其他東西來保護(hù)它。

獲取特定于處理程序的依賴項的參數(shù)

如果特定處理程序具有依賴項,請將其作為參數(shù)。

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

格式變量可供處理程序訪問。

處理程序上的HandlerFunc

我現(xiàn)在幾乎在所有情況下都使用http.HandlerFunc,而不是http.Handler。

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

它們或多或少是可以互換的,所以只需選擇更容易閱讀的內(nèi)容。 對我來說,這是http.HandlerFunc。

中間件只是一個Go函數(shù)

中間件函數(shù)接受一個http.HandlerFunc并返回一個可以在調(diào)用原始處理程序之前和/或之后運行代碼的新函數(shù) - 或者它可以決定根本不調(diào)用原始處理程序。

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)
    }
}

處理程序內(nèi)部的邏輯可以選擇是否調(diào)用原始處理程序 - 在上面的示例中,如果IsAdmin為false,處理程序?qū)⒎祷豀TTP 404 Not Found并返回(abort); 注意沒有調(diào)用h處理程序。
如果IsAdmin為true,則執(zhí)行將傳遞給傳入的h處理程序。
通常我在routes.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()))
}

請求和響應(yīng)類型也可以在那里

如果一個接口有自己的請求和響應(yīng)類型,通常它們僅對該特定處理程序有用。
如果是這種情況,您可以在函數(shù)內(nèi)定義它們。

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) {
        ...
    }
}

這會對您的包空間進(jìn)行整理,并允許您將這些類型命名為相同,而不必考慮特定于處理程序的版本。
在測試代碼中,您只需將類型復(fù)制到測試函數(shù)中并執(zhí)行相同的操作即可。 要么…

測試類型可以幫助構(gòu)建測試

如果您的請求/響應(yīng)類型隱藏在處理程序中,您只需在測試代碼中聲明新類型即可。
這是一個為需要了解您的代碼的后代做一些故事講述的機會。
例如,假設(shè)我們的代碼中有Person類型,我們在許多接口上重用它。 如果我們有一個/ greet接口,我們可能只關(guān)心他們的名字,所以我們可以在測試代碼中表達(dá):

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

從這個測試中可以清楚地看出,我們唯一關(guān)心的事情是人的名字。

sync.Once設(shè)置依賴項

如果我在準(zhǔn)備處理程序時必須做任何昂貴的事情,我會推遲到第一次調(diào)用該處理程序時。
這提高了應(yīng)用程序啟動時間

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確保代碼只執(zhí)行一次,其他調(diào)用(其他人發(fā)出相同的請求)將一直阻塞,直到完成。

  1. 錯誤檢查在init函數(shù)之外,所以如果出現(xiàn)問題我們?nèi)匀粫霈F(xiàn)錯誤并且不會在日志中丟失它
    2.如果未調(diào)用處理程序,則永遠(yuǎn)不會完成昂貴的工作 - 這可能會帶來很大的好處,具體取決于代碼的部署方式
    請記住,執(zhí)行此操作時,您將初始化時間從啟動時移至運行時(首次訪問端點時)。 我經(jīng)常使用Google App Engine,所以這對我來說很有意義,但是你的情況可能會有所不同,所以值得思考何時何地使用sync.Once這樣。

服務(wù)器是可測試的

我們的服務(wù)器類型非常便于測試。

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)
}

在每個測試中創(chuàng)建一個服務(wù)器實例 - 如果昂貴的東西延遲加載,這將不會花費太多時間,即使對于大組件
通過在服務(wù)器上調(diào)用ServeHTTP,我們正在測試整個堆棧,包括路由和中間件等。如果你想避免這種情況,你當(dāng)然可以直接調(diào)用處理程序方法。
使用httptest.NewRecorder記錄處理程序正在執(zhí)行的操作
此代碼示例使用我的測試迷你框架(作為Testify的迷你替代品)

結(jié)論

我希望本文中涉及的內(nèi)容有意義,并幫助您完成工作。 如果您不同意或有其他想法,請發(fā)推特給我。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容