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

圖片描述

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

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

1. Server Struct

個人全部組件都有一個server結構,一般看起來像這樣:服務器

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

共享依賴項是結構的字段閉包

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

這很方便,由於大多數代碼維護都是從URL錯誤報告開始的,因此只需一眼就routes.go能夠指示咱們在哪裏查看.框架

3. server 掛載 handler

個人HTTPserver 掛載 handler函數

func(s * server)handleSomething()http.HandlerFunc {...}
handler能夠經過s服務器變量訪問依賴項.測試

4. return Handler

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

這給了咱們一個閉包環境,咱們的處理程序能夠在其中運行3d

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

相關文章
相關標籤/搜索