我這幾年來是如何編寫 Go HTTP 服務的

來源:互聯網
上載者:User
我是從 [r59](https://golang.org/doc/devel/pre_go1.html#r59) —— 1.0 之前的一個發布版本,就開始寫 Go 了,並且在過去七年裡一直在用 Go 構建 HTTP API 和服務。在 [Machine Box](https://machinebox.io/?utm_source=matblog-3May2018&utm_medium=matblog-3May2018&utm_campaign=matblog-3May2018&utm_term=matblog-3May2018&utm_content=matblog-3May2018) 裡,我大部分的技術性工作涉及到構建各種各樣的 API。 機器學習本身很複雜而且大部分開發人員也不會用到,所以我的工作就是通過 API 終端來簡單闡述一下,目前來說反響很不錯。> 如果你還沒有看過 Machine Box 開發人員的經驗, [請嘗試一下](https://machinebox.io/docs/facebox/teaching-facebox) 並讓我知道你的意見。我編寫服務的方法已經在過去幾年中發生了變化,所以我打算分享目前我編寫服務的經驗——也許這些方法能幫到你和你的工作。## 一個 server 結構體我所有的組件都有一個單獨的 `server` 結構體,它通常都是類似於下面這種形式:```gotype server struct { db *someDatabase router *someRouter email EmailSender}```-公用組件是該結構體的欄位。## routes.go在每個組件中我有個一個唯一的檔案 `routes.go` ,在這裡所有的路由都能運行。```gopackage appfunc (s *server) routes() { s.router.HandleFunc("/api/", s.handleAPI()) s.router.HandleFunc("/about", s.handleAbout()) s.router.HandleFunc("/", s.handleIndex())}```這樣會很方便,因為大部分代碼維護都開始於一個 URL 和一個被報告的錯誤——所以只要瀏覽一下 `routes.go` 就能引導我們到目的地。## 掛起伺服器的 handler我的 HTTP handler 掛起伺服器:```gofunc (s *server) handleSomething() http.HandlerFunc { ... }```handler 可以通過 s 這個server變數來訪問依賴項。## 返回 handler我的 handler 函數不會處理請求,它們返回的函數完成處理工作。這樣會提供給我們一個 handler 可以啟動並執行封閉環境。```gofunc (s *server) handleSomething() http.HandlerFunc { thing := prepareThing() return func(w http.ResponseWriter, r *http.Request) { // use thing }}````prepareThing` 函數只會被調用一次,所以你可以用它來完成每個 handler 的一次性的初始化工作,然後在 handler 裡面使用 `thing` 。請確保只會對共用資料執行讀操作,如果 handler 改寫了共用資料,記住你需要用鎖或者其他機制來保護共用資料。## 為 handler 專有的依賴傳遞參數如果一個特別的 handler 有一個依賴項,就把這個依賴項當作參數。```gofunc (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` 。```gofunc (s *server) handleSomething() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ... }}```兩者之間大都可以互換,所以覺得哪個便於閱讀就選哪個即可。對我來說,`http.HandlerFunc` 更加適合。## 中介軟體僅僅只是 Go 函數中介軟體函數接受一個  `http.HandlerFunc` 並且返回一個新的 HandlerFunc , 該 handler 可以在調用初始 handler 之前或之後運行代碼——抑或它可以決定是否調用初始的 handler 。```gofunc (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 內部的邏輯可以選擇性的決定是否調用初始 handler——在上面的例子裡,如果  `IsAdmin` 為 `false`,該 handler 就會返回一個 HTTP  `404 Not Found` 並且返回(中止);注意,`h` handler 沒有被調用。如果 `IsAdmin` 為 `true`, 就會運行到 `h` handler。通常我會把中介軟體放到 `routes.go` 檔案中:```gopackage appfunc (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))}```## request 和 response 類型也可以放在那裡如果終端有它自身的 request 和 response 類型的話,通常這些類型只對特定的 handler 有用。假設一個例子,你可以把它們定義在函數內部。```gofunc (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) { ... }}```這樣就可以解放包的空間,並允許你把這種類型都定義成同樣的名字,從而免去了特定的 handler 考慮命名。在測試代碼時,你可以直接複製這些類型到你的測試函數中並執行同樣的操作。或者其他……## 測試類型有助於架構測試的架構如果你的 request/response 類型都隱藏在 handler 內部,那麼你可以在測試代碼中直接定義新的類型。這就有機會做一些解釋性的工作,以便讓未來的接任者能夠理解你的代碼。舉個例子,我們假設代碼中一個 `Person` 類型存在,並且在很多終端都會重用它。如果我們有一個 `/greet` 終端,這時可能只關心它的 `Name`,所以可以在測試代碼中這樣表述:```gofunc 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```從測試代碼中可以清晰的看出,我們只關心 `Person` 的 `Name` 欄位。## sync.Once 組織依賴如果我不得不為準備 handler 時執行一些代價高昂的操作,我就會把它們延遲到第一次調用 handler 的時刻。這樣可以改善應用的啟動時間。```gofunc (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` 函數外面,所以如果出現錯誤的話我們依然可以捕獲它,而且不會在日誌中遺失 。- 如果 handler 沒有被調用過,這些代價高昂的操作就永遠不會發生——這可以對你的代碼部署有極大好處。記住這一點,上面是把初始化的時間從啟動時刻移到了運行時刻(當端點第一次被訪問到時)。我使用 Google App Engine 很久了,對我來說這種操作是可以理解的,但對你自身來說可能就未必了。所以你有必要思考何時何地值得用 `sync.Once` 這種方式。## server 必須易於測試我們的 server 類型需要能夠簡單測試。```gofunc 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, r) is.Equal(w.StatusCode, http.StatusOK)}```- 在每組測試中建立一個 server 執行個體——如果把代價高昂的操作消極式載入,這就不會花費太多時間,即使是對大型組件也依然有效。- 通過調用伺服器上的 ServerHTTP ,我們會測試到整個棧,包括路由和中介軟體等。當然了,如果希望避免這種情況的話,你也可以直接調用 handler 函數。- 使用 `httptest.NewRecorder` 來記錄 handler 所執行的操作。- 這份程式碼範例使用到了我的 [一個正在測試中的微架構](https://godoc.org/github.com/matryer/is) (一個驗證用的簡易可選項)## 結論我希望文章中涵蓋到的內容可能對你有些用處,能協助到你的工作。如果你有不同意見或其它想法的話, [請聯絡我們](https://twitter.com/matryer) 。

via: https://medium.com/statuscode/how-i-write-go-http-services-after-seven-years-37c208122831

作者:Mat Ryer 譯者:sunzhaohao 校對:polaris1119

本文由 GCTT 原創編譯,Go語言中文網 榮譽推出

本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽

482 次點擊  
相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.