用無上下文的Go語言實現HTTP服務
許多Go開發者,尤其是新開發者,發現一個不明顯的問題是,我到底該如何把所有我需要的東西都傳到我的處理程式中?
我們沒有像Java或C那樣花哨的控制反轉系統。 http.處理程式是靜態簽名,所以我不能只傳遞我真正想要的東西。看來我們只有3個選擇:使用globals、將處理程式包裹在一個函式中,或者在context.Context中傳遞東西。
這裡舉例是電子商務:
任務是寫一個端點,給定某個類別的ID,返回該類別的物品列表。
這個端點需要訪問我們的
- items.Serviceto 來做實際的查詢,
- logging.Service 是用來以防出錯,
- 還有 metrics.Service 來獲得甜蜜的營銷指標。
讓我們來看看這三種選擇:
1、全域性變數
我們在處理程式中的第一次嘗試是使用全域性變數globals。這是一種相當自然的方式,許多初學者都傾向於這樣做。
func GetItemsInCategory(w http.ResponseWriter, r *http.Request) { categoryID := // get the id in your favorite way possible (net/http, mux, chi, etc) metrics.Service.Increment("category_lookup", categoryID) items, err := items.Service.LookupByCategory(categoryID) if err != nil { logging.Service.Logf("failed to get items for category, %d: %w", categoryID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } err := json.NewEncoder(w).Encode(items) if err != nil { logging.Service.Logf("failed to get items for category, %d: %w", categoryID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } } |
我們都曾經寫過這樣的程式碼。然後我們很快就知道,全域性變數並不好的,因為它們會使程式碼變得不靈活,無法測試。
這自然而然地導致了一個由書籍、文章、影片等組成的兔子洞,告訴你 "注入你的依賴關係!"。
這段程式碼是不可能的,讓我們嘗試一下注入。
2. 依賴性注入|獲得你的助推器
好吧,我們不想使用globals全域性變數,但http.Handler有一個固定的簽名。那麼我們該怎麼做呢?當然是包裹它
func GetItemsInCategory(itemsService items.Service, metricsService metrics.Service, loggingService logging.Service) http.Handler { return http.HandlerFunc(func GetItemsInCategory(w http.ResponseWriter, r *http.Request) { categoryID := // get the id in your favorite way possible (net/http, mux, chi, etc) metricsService.Increment("category_lookup", categoryID) items, err := itemsService.LookupByCategory(categoryID) if err != nil { logging.Service.Logf("failed to get items for category, %d: %w", categoryID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } err := json.NewEncoder(w).Encode(items) if err != nil { loggingService.Logf("failed to get items for category, %d: %w", categoryID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } }) } |
這樣做比較好,因為我們不再依賴全域性狀態,但它仍然有很多地方需要改進。主要是,它使每一個處理程式都變成了一個大的、長的、令人討厭的混亂的編寫。很容易想象,如果我們再增加幾個服務,這個簽名就會延長2-3倍。
所以我們閱讀了一下,發現我們的*http.Request裡面有一個context.Context! 我們可以建立一箇中介軟體,注入我們所有的依賴項,然後處理程式可以直接提取它需要的東西!這肯定會解決我們所有的問題。
2. 引入Context上下文
首先,讓我們製作我們所談論的中介軟體。我們只是要在設定好所有的依賴關係後再進行內聯。
// initialize things contextMiddleware := func(h http.Handler) http.Handler { return http.HandlerFunc(w http.ResponseWriter, r *http.Request) { ctx := r.Context() ctx = context.WithValue(ctx, CtxKeyLogger, loggingService) ctx = context.WithValue(ctx, CtxKeyItems itemsService) ctx = context.WithValue(ctx, CtxKeyMetrics, metricsService) ... // add ALL the services we would ever need! h.ServeHTTP(w, r.WithContext(ctx)) }) } // register the middleware |
現在,我們已經新增了所有這些內容,我們可以再次重做我們的處理程式。
func GetItemsInCategory(w http.ResponseWriter, r *http.Request) { ctx := r.Context() loggingService := ctx.Value(CtxKeyLogging).(logging.Service) metricsService := ctx.Value(CtxKeyMetrics).(metrics.Service) itemsService := ctx.Value(CtxKeyItems).(items.Service) categoryID := // get the id in your favorite way possible (net/http, mux, chi, etc) metricsService.Increment("category_lookup", categoryID) items, err := itemsService.LookupByCategory(categoryID) if err != nil { loggingService.Logf("failed to get items for category, %d: %w", categoryID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } err := json.NewEncoder(w).Encode(items) if err != nil { logging.Service.Logf("failed to get items for category, %d: %w", categoryID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } } |
不錯,很容易! 這裡不會出什麼問題。好吧,除了在3個月後,有人在應用程式的其他地方移動了一行程式碼,這些服務中的一個不再存在於context中。
你可能會說:"好吧,聰明的先生,我只需要檢查一下確保即可!"。
func GetItemsInCategory(w http.ResponseWriter, r *http.Request) { ctx := r.Context() loggingService, ok := ctx.Value(CtxKeyLogging).(logging.Service) if ok != nil { // We dont have a logging service to log to!!! http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } metricsService, ok := ctx.Value(CtxKeyMetrics).(metrics.Service) if ok != nil { loggingService.Logf("metrics service not in context!", categoryID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } itemsService, ok := ctx.Value(CtxKeyItems).(items.Service) if ok != nil { loggingService.Logf("items service not in context!", categoryID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } categoryID := // get the id in your favorite way possible (net/http, mux, chi, etc) metricsService.Increment("category_lookup", categoryID) items, err := itemsService.LookupByCategory(categoryID) if err != nil { loggingService.Logf("failed to get items for category, %d: %w", categoryID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } err := json.NewEncoder(w).Encode(items) if err != nil { logging.Service.Logf("failed to get items for category, %d: %w", categoryID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } } |
正如你所看到的,即使在小的、簡單的、設計好的例子中,以安全的方式來做這件事也變得有點混亂。
我們使用Go的原因之一就是為了避免所有這些型別檢查的混亂! 透過使用上下文作為我們的依賴關係的某種抓包,我們有效地放棄了型別安全。此外,我們在執行時才知道事情是否存在。為了解決這個問題,我們最終會發現到處都是長串的錯誤檢查。
這可不好。我們的三個選項都以各自的方式糟糕。如果我們想注入我們的依賴關係,似乎我們無法逃離冗長的地獄。當然,你可以繞過這些問題,建立花哨的函式來包裝很多東西,但它實際上並沒有解決問題。
banq注:依賴注入後還需要檢測?在java中從來不檢測,執行時會報錯,沒有注入,認為需要手工檢測是一種過度思考,或者不熟悉IOC/DI概念以後的焦慮表現吧?
4. Structs
你們中的一些人可能已經對我大喊大叫,讓我說到這個問題,但是在教過這個課題好幾次之後,先看看其他的解決方案確實有幫助。
我們沒有考慮的 "第四種方法 "其實很簡單。
我們建立一個結構來儲存我們需要的依賴關係。
我們將我們的處理程式作為方法新增到該結構中。
讓我們來看看一個例子。
type CategoryHandler struct { metrics metrics.Service logger logging.Service items items.Service } func (h *CategoryHandler) GetItemsInCategory(w http.ResponseWriter, r *http.Request) { categoryID := // get the id in your favorite way possible (net/http, mux, chi, etc) h.metrics.Increment("category_lookup", categoryID) items, err := h.items.LookupByCategory(categoryID) if err != nil { h.logger.Logf("failed to get items for category, %d: %w", categoryID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } err := json.NewEncoder(w).Encode(items) if err != nil { h.logger.Logf("failed to get items for category, %d: %w", categoryID, err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } } |
這個解決方案有幾個很大的好處。
- 我們所依賴的一切在構建時都是已知的,並且是型別安全的(與上下文不同)。
- 我們只有最少的額外模板(不像封裝函式)。
- 保持可測試性(與globals不同)
- 允許我們將相關的處理程式 "分組group "為單元
第4條是我們還沒有觸及的一個問題,但是現在我們有了這個結構,我們就可以對有共同依賴關係的處理程式進行分組,或者從邏輯上將它們放在一起。
例如,我們可以在這裡新增一個處理程式來新增一個新的類別。我們可以建立一個MetricsHandler,將所有與度量相關的端點組合在一起。你可以根據自己的意願,對其進行細化或擴充套件(在大多數情況下,細化可能更好)。
banq注:這個方法也是一種依賴注入,不過是建構函式形式的依賴注入,靜態上下文是一種類似JavaBeans的setterX方法注入依賴。靜態上下文的注入見下面連結:
相關文章
- 用 GitHub Actions 部署 Go 語言服務GithubGo
- 用Go語言寫HTTP中介軟體GoHTTP
- 使用 Go 語言建立 WebSocket 服務GoWeb
- 使用Go語言建立WebSocket服務GoWeb
- Go語言實現RPCGoRPC
- go語言實現自己的RPC:go rpc codecGoRPC
- go語言實現掃雷Go
- 使用Go語言開發短地址服務Go
- Go語言map的底層實現Go
- Go語言實現的Java Stream APIGoJavaAPI
- go語言gRPC系列(三) - 使用grpc-gateway同時提供HTTP和gRPC服務GoRPCGatewayHTTP
- go語言遊戲服務端開發(三)——服務機制Go遊戲服務端
- 用 Go + WebSocket 快速實現一個 chat 服務GoWeb
- go語言實現ssh打隧道Go
- Go語言interface底層實現Go
- go語言依賴注入實現Go依賴注入
- Go語言實現TCP通訊GoTCP
- GO語言 實現埠掃描Go
- 如何用GO語言編寫快取服務?Go快取
- Go語言HTTP/2探險之旅GoHTTP
- Go 標準庫 http.FileServer 實現靜態檔案服務GoHTTPServer
- 從零到一:用Go語言構建你的第一個Web服務GoWeb
- Go語言實現excel匯入無限級選單結構GoExcel
- go微服務系列(三) - 服務呼叫(http)Go微服務HTTP
- 用 Go 語言實戰 Limit Concurrency 方法GoMIT
- Go語言實現HTTPS加密協議GoHTTP加密協議
- 檔案複製(Go語言實現)Go
- 線性迴歸 go 語言實現Go
- 漏桶、令牌桶限流的Go語言實現Go
- Go語言微服務開發框架實踐-go chassis(上篇)Go微服務框架
- Go語言微服務開發框架實踐-go chassis(中篇)Go微服務框架
- Go語言核心36講(Go語言實戰與應用二十)--學習筆記Go筆記
- Go語言核心36講(Go語言實戰與應用十九)--學習筆記Go筆記
- Go語言核心36講(Go語言實戰與應用十八)--學習筆記Go筆記
- Go語言核心36講(Go語言實戰與應用十七)--學習筆記Go筆記
- Go語言核心36講(Go語言實戰與應用十三)--學習筆記Go筆記
- Go語言核心36講(Go語言實戰與應用十四)--學習筆記Go筆記
- Go語言核心36講(Go語言實戰與應用十五)--學習筆記Go筆記