趁着元旦休假+春節,嘗試把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
以外;不過我要聲明,這樣處理是將初始化啓動時推遲到了運行時(首次訪問)。由於我常用 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) }
srv.ServeHTTP
時實際上是在測試整個調用棧了,也包括路由、中間件等等。若是想避免所有都調用,你也能夠直接調用對應的handler;httptest.NewRecorder
記錄handler都幹了啥;總結
我但願文章內容對你有幫助,若是不一樣意本文觀點或者有其餘想法都歡迎在Twitter上和我討論。