Go語言HTTP服務最佳實踐(譯)

圖片描述

自從go語言r59版本(一個1.0之前的版本)以來,我一直在寫Go程序,並且在過去七年裏一直在Go中構建HTTP API和服務.

多年來,我編寫服務的方式發生了變化,所以我想分享今天如何編寫服務 - 以防模式對您和您的工作有用.

1. Server Struct

我的所有組件都有一個server結構,通常看起來像這樣:

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

共享依賴項是結構的字段

2. 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())
}

這很方便,因爲大多數代碼維護都是從URL錯誤報告開始的,所以只需一眼就routes.go可以指示我們在哪裏查看.

3. server 掛載 handler

我的HTTPserver 掛載 handler

func(s * server)handleSomething()http.HandlerFunc {...}
handler可以通過s服務器變量訪問依賴項.

4. return Handler

我的處理函數實際上並不處理Request,它們返回一個handler函數.

這給了我們一個閉包環境,我們的處理程序可以在其中運行

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

prepareThing只調用一次,所以你可以用它做一次每處理程序初始化,然後用thing在處理程序.

確保只讀取共享數據,如果處理程序正在修改任何內容,請記住您需要一個互斥鎖或其他東西來保護它.

5. 參數是handler函數的依賴

如果特定處理程序具有依賴項,請將其作爲參數.


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

format處理程序可以訪問該變量.

6. HandlerFunc over Handler

http.HandlerFunc現在幾乎用在每一個案例中,而不是http.Handler.

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

它們或多或少是可以互換的,所以只需選擇更容易閱讀的內容.對我來說,就是這樣http.HandlerFunc.

5. Middleware中間件

中間件函數接受http.HandlerFunc並返回一個可以在調用原始處理程序之前和/或之後運行代碼的新函數 - 或者它可以決定根本不調用原始handler.

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

處理程序內部的邏輯可以選擇是否調用原始處理程序 - 在上面的示例中,如果IsAdminfalse,HandlerFunc將返回HTTP 404 Not Found並返回(abort); 注意沒有調用h處理程序.

如果IsAdmintrue,則將執行傳遞給傳入的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()))
}

7. Request 和 Response類

如果Server有自己的請求響應類型,通常它們僅對該特定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){ 
        . .. 
    } 
}

這會對您的包空間進行整理,並允許您將這些類型命名爲相同,而不必考慮特定於處理程序的版本.

在測試代​​碼中,您只需將類型複製到測試函數中並執行相同的操作即可.要麼…

8. 測試框架

如果您的請求/響應類型隱藏在處理程序中,您只需在測試代碼中聲明新類型即可.

這是一個爲需要了解您的代碼的後代做一些故事講述的機會.

例如,假設Person我們的代碼中有一個類型,我們在許多端點上重用它.如果我們有一個/greetendpoint,我們可能只關心他們的名字,所以我們可以在測試代碼中表達:

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)
    / / ...這裏有更多測試代碼

從這個測試中可以清楚地看出,我們關心的唯一領域就是Name人.

9. sync.Once 配置依賴項

如果我在準備處理程序時必須做任何昂貴的事情,我會推遲到第一次調用該處理程序時.

這改善了應用程序啓動時間

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

10. sync.Once 確保代碼只執行一次

其他調用(其他人發出相同的請求)將一直阻塞,直到完成.
錯誤檢查在init函數之外,所以如果出現問題我們仍然會出現錯誤,並且不會在日誌中丟失錯誤
如果未調用處理程序,則永遠不會完成昂貴的工作 - 根據代碼的部署方式,這可能會帶來很大的好處
請記住,執行此操作時,您將初始化時間從啓動時移至運行時(首次訪問端點時).我經常使用Google App Engine,所以這對我來說很有意義,但是你的情況可能會有所不同,所以值得思考何時何地使用sync.Once這樣.

11. 服務器是可測試的

我們的服務器類型非常可測試.

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(錯誤)
    w:= httptest.NewRecorder()
    srv.ServeHTTP(w,req)
    is.Equal(w.StatusCode,http.StatusOK)
}

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

原文地址

(譯)Go語言HTTP服務最佳實踐-tech.mojotv

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