用無上下文的Go語言實現HTTP服務

banq發表於2022-04-09

許多Go開發者,尤其是新開發者,發現一個不明顯的問題是,我到底該如何把所有我需要的東西都傳到我的處理程式中?
我們沒有像Java或C那樣花哨的控制反轉系統。 http.處理程式是靜態簽名,所以我不能只傳遞我真正想要的東西。看來我們只有3個選擇:使用globals、將處理程式包裹在一個函式中,或者在context.Context中傳遞東西。

這裡舉例是電子商務:
任務是寫一個端點,給定某個類別的ID,返回該類別的物品列表。
這個端點需要訪問我們的
  1. items.Serviceto 來做實際的查詢,
  2. logging.Service 是用來以防出錯,
  3. 還有 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方法注入依賴。靜態上下文的注入見下面連結:

相關文章