自從go語言r59版本(一個1.0以前的版本)以來,我一直在寫Go程序,而且在過去七年裏一直在Go中構建HTTP API和服務.json
多年來,我編寫服務的方式發生了變化,因此我想分享今天如何編寫服務 - 以防模式對您和您的工做有用.api
個人全部組件都有一個server
結構,一般看起來像這樣:服務器
type server struct { db * someDatabase router * someRouter email EmailSender }
共享依賴項是結構的字段閉包
我在每一個組件中都有一個文件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
能夠指示咱們在哪裏查看.框架
個人HTTP
server 掛載 handler
:函數
func(s * server)handleSomething()http.HandlerFunc {...}
handler能夠經過s服務器變量訪問依賴項.測試
個人處理函數實際上並不處理Request,它們返回一個handler函數.spa
這給了咱們一個閉包環境
,咱們的處理程序能夠在其中運行3d
func(s * server)handleSomething()http.HandlerFunc { thing:= prepareThing() return func(w http.ResponseWriter,r * http.Request){ // use thing } }
該prepareThing
只調用一次,因此你能夠用它作一次每處理程序初始化,而後用thing在處理程序.
確保只讀取共享數據,若是處理程序正在修改任何內容,請記住您須要一個互斥鎖或其餘東西來保護它.
若是特定處理程序具備依賴項,請將其做爲參數.
func(s * server)handleGreeting(format string)http.HandlerFunc { return func(w http.ResponseWriter,r * http.Request){ fmt.Fprintf(w,format,"World") } }
format處理程序能夠訪問該變量.
Handler
我http.HandlerFunc
如今幾乎用在每個案例中,而不是http.Handler
.
func(s * server)handleSomething()http.HandlerFunc { return func(w http.ResponseWriter,r * http.Request){ ... } }
它們或多或少是能夠互換的,因此只需選擇更容易閱讀的內容.對我來講,就是這樣http.HandlerFunc.
中間件函數接受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) } }
處理程序內部的邏輯能夠選擇是否調用原始處理程序 - 在上面的示例中,若是IsAdmin
是false
,HandlerFunc將返回HTTP 404 Not Found
並返回(abort
); 注意沒有調用h
處理程序.
若是IsAdmin
是true
,則將執行傳遞給傳入的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())) }
若是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){ . .. } }
這會對您的包空間
進行整理,並容許您將這些類型命名爲相同
,而沒必要考慮特定於處理程序的版本.
在測試代碼中,您只需將類型複製到測試函數中並執行相同的操做便可.要麼…
若是您的請求/響應
類型隱藏在處理程序中,您只需在測試代碼中聲明新類型便可.
這是一個爲須要瞭解您的代碼的後代作一些故事講述的機會.
例如,假設Person咱們的代碼中有一個類型,咱們在許多端點上重用它.若是咱們有一個/greet
endpoint,咱們可能只關心他們的名字,因此咱們能夠在測試代碼中表達:
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人.
若是我在準備處理程序時必須作任何昂貴的事情,我會推遲到第一次調用該處理程序時.
這改善了應用程序啓動時間
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 } }
其餘調用(其餘人發出相同的請求)將一直阻塞,直到完成.
錯誤檢查在init函數以外,因此若是出現問題咱們仍然會出現錯誤,而且不會在日誌中丟失錯誤
若是未調用處理程序,則永遠不會完成昂貴的工做 - 根據代碼的部署方式,這可能會帶來很大的好處
請記住,執行此操做時,您將初始化時間從啓動時移至運行時(首次訪問端點時).我常用Google App Engine,因此這對我來講頗有意義,可是你的狀況可能會有所不一樣,因此值得思考什麼時候何地使用sync.Once這樣.
咱們的服務器類型很是可測試.
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
的迷你替代品)