原文:https://medium.com/statuscode/how-i-write-go-http-services-after-seven-years-37c208122831
翻譯:devabel
自從r59(一個1.0之前的版本)以來,我一直在寫Go(那時還不叫Golang),并且在過去的七年里一直在Go中構(gòu)建HTTP API和服務(wù)。
我在Machine Box工作,我的大多數(shù)技術(shù)工作都涉及構(gòu)建各種API。 機器學(xué)習(xí)很復(fù)雜,大多數(shù)開發(fā)人員都無法掌握,因此我的工作是通過API接口簡化這個過程,到目前為止我們已經(jīng)得到了很好的反饋。
如果您還沒有親眼嘗試過Machine Box開發(fā)者的體驗,請試一試,讓我知道您的想法。
多年來,我編寫服務(wù)的方式發(fā)生了變化,因此今天我想分享如何編寫服務(wù) - 也許這種方式對您的工作有幫助。
A server struct
我的所有組件都有一個 server結(jié)構(gòu)體,通??雌饋硐襁@樣:
type server struct {
db *someDatabase
router *someRouter
email EmailSender
}
共享依賴項是結(jié)構(gòu)的字段
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())
}
這很方便,因為大多數(shù)代碼維護(hù)都是以URL和錯誤報告開始的 - 所以只需瀏覽一下routes.go即可幫助我們調(diào)試。
處理程序掛起服務(wù)器
我的HTTP處理程序掛起了服務(wù)器:
func (s *server) handleSomething() http.HandlerFunc { ... }
處理程序可以通過s服務(wù)器變量訪問依賴項。
返回處理程序
我的處理函數(shù)實際上并不處理請求,它們返回一個函數(shù)。
這給了我們一個閉包環(huán)境,我們的處理程序可以在其中運行
func (s *server) handleSomething() http.HandlerFunc {
thing := prepareThing()
return func(w http.ResponseWriter, r *http.Request) {
// use thing
}
}
prepareThing僅被調(diào)用一次,因此您可以使用它來執(zhí)行一次性每個處理程序初始化,然后在處理程序中使用該事物。
確保只讀取共享數(shù)據(jù),如果處理程序正在修改任何內(nèi)容,請記住您需要一個互斥鎖或其他東西來保護(hù)它。
獲取特定于處理程序的依賴項的參數(shù)
如果特定處理程序具有依賴項,請將其作為參數(shù)。
func (s *server) handleGreeting(format string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, format, "World")
}
}
格式變量可供處理程序訪問。
處理程序上的HandlerFunc
我現(xiàn)在幾乎在所有情況下都使用http.HandlerFunc,而不是http.Handler。
func (s *server) handleSomething() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
...
}
}
它們或多或少是可以互換的,所以只需選擇更容易閱讀的內(nèi)容。 對我來說,這是http.HandlerFunc。
中間件只是一個Go函數(shù)
中間件函數(shù)接受一個http.HandlerFunc并返回一個可以在調(diào)用原始處理程序之前和/或之后運行代碼的新函數(shù) - 或者它可以決定根本不調(diào)用原始處理程序。
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)
}
}
處理程序內(nèi)部的邏輯可以選擇是否調(diào)用原始處理程序 - 在上面的示例中,如果IsAdmin為false,處理程序?qū)⒎祷豀TTP 404 Not Found并返回(abort); 注意沒有調(diào)用h處理程序。
如果IsAdmin為true,則執(zhí)行將傳遞給傳入的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()))
}
請求和響應(yīng)類型也可以在那里
如果一個接口有自己的請求和響應(yīng)類型,通常它們僅對該特定處理程序有用。
如果是這種情況,您可以在函數(shù)內(nèi)定義它們。
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) {
...
}
}
這會對您的包空間進(jìn)行整理,并允許您將這些類型命名為相同,而不必考慮特定于處理程序的版本。
在測試代碼中,您只需將類型復(fù)制到測試函數(shù)中并執(zhí)行相同的操作即可。 要么…
測試類型可以幫助構(gòu)建測試
如果您的請求/響應(yīng)類型隱藏在處理程序中,您只需在測試代碼中聲明新類型即可。
這是一個為需要了解您的代碼的后代做一些故事講述的機會。
例如,假設(shè)我們的代碼中有Person類型,我們在許多接口上重用它。 如果我們有一個/ greet接口,我們可能只關(guān)心他們的名字,所以我們可以在測試代碼中表達(dá):
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
從這個測試中可以清楚地看出,我們唯一關(guān)心的事情是人的名字。
sync.Once設(shè)置依賴項
如果我在準(zhǔn)備處理程序時必須做任何昂貴的事情,我會推遲到第一次調(diào)用該處理程序時。
這提高了應(yīng)用程序啟動時間
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確保代碼只執(zhí)行一次,其他調(diào)用(其他人發(fā)出相同的請求)將一直阻塞,直到完成。
- 錯誤檢查在init函數(shù)之外,所以如果出現(xiàn)問題我們?nèi)匀粫霈F(xiàn)錯誤并且不會在日志中丟失它
2.如果未調(diào)用處理程序,則永遠(yuǎn)不會完成昂貴的工作 - 這可能會帶來很大的好處,具體取決于代碼的部署方式
請記住,執(zhí)行此操作時,您將初始化時間從啟動時移至運行時(首次訪問端點時)。 我經(jīng)常使用Google App Engine,所以這對我來說很有意義,但是你的情況可能會有所不同,所以值得思考何時何地使用sync.Once這樣。
服務(wù)器是可測試的
我們的服務(wù)器類型非常便于測試。
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)
}
在每個測試中創(chuàng)建一個服務(wù)器實例 - 如果昂貴的東西延遲加載,這將不會花費太多時間,即使對于大組件
通過在服務(wù)器上調(diào)用ServeHTTP,我們正在測試整個堆棧,包括路由和中間件等。如果你想避免這種情況,你當(dāng)然可以直接調(diào)用處理程序方法。
使用httptest.NewRecorder記錄處理程序正在執(zhí)行的操作
此代碼示例使用我的測試迷你框架(作為Testify的迷你替代品)
結(jié)論
我希望本文中涉及的內(nèi)容有意義,并幫助您完成工作。 如果您不同意或有其他想法,請發(fā)推特給我。