請教下,go HTTP 服務如何同時支援 GET 與 POST

語言: CN / TW / HK

一、背景

專案裡有個讀介面是 http 協議。

由於舊的 CGI 框架不區分 GET 和 POST,兩種都支援。

去年一個小夥伴使用 go 語言進行了重構上雲,即用 go 開發了一個 http 服務部署到容器平臺上。

瀏覽器域名來的流量,切換的時候都正常,流量也順利切換了。

切換到後臺服務的流量時,發現有個 php 業務沒有使用 GET 請求資料,而是使用的 POST 請求。

這個小夥伴馬上修改程式碼,相容了 POST 請求。

之後在正式環境的名字服務上配置了權重為 1 的流量。

恰好那幾天團隊發生調整,小夥伴離開了。

就這樣,新的 HTTP 服務使用權重為 1 的流量在正式環境跑了幾個月。

二、降本增效發現問題

最近快過年了,大家都在為降低成本發愁,老闆問這個舊的 HTTP 服務能不能下線。

本來我想,還有兩週就過年了,還是不要動了,年後再說。

後來又一想,舊的 HTTP 服務還是傳統的實體機,流量暴漲的話無法自動擴容,年前就全部上雲也沒問題。

於是發了一個專案變更周知公告:HTTP服務重構上雲,之前權重很低跑了幾個月了,這幾天會增加流量。

結果操作半天之後,有人反饋自己的服務讀不到資料了(失敗率升高暴露問題)。

細問得知,這個業務使用的 POST 請求來讀資料。

於是我便回滾了這個業務訪問的名字服務下的流量。

之後,我找到塵封很久的 GO 開發的 HTTP 服務程式碼。

找程式碼的時候,我還在納悶,記得曾經小夥伴解決了這個問題的,怎麼還有這個問題呢?

vscode 開啟程式碼後,看了眼 post 請求的引數處理邏輯,一眼便看出原因來。

GET 請求的格式是 k1=v1&k2=v2 的形式。

POST 的 BODY 裡的格式正常情況下也是 k1=v1&k2=v2 的形式。

小夥伴卻把 POST 的請求資料當做 JSON 格式,去解析 JSON 了,那肯定解不開了。

所以,POST 請求就全部報引數非法錯誤了。

三、最原始的方法

看程式碼可以發現,具體實現的時候,會根據 Method 來回調不同的處理函式。

我們需要做的是不解析 JSON,而是解析類似於 k1=v1&k2=v2 的字串。

所以修改完的程式碼就是下面的樣子。

func postParamsToQuery(r *http.Request) url.Values {
  body := io.ReadAll(r.Body)
  post := doubleSplit(body, '&', '=')
  return trimMapValues(post)
}

四、複用 URL 庫

當然,實際上我不會去實現上面的解析函式的。

因為 URL 庫肯定實現了這個功能。

於是我閱讀了 net/url 庫的全部原始碼,發現 url 庫果然自帶這個功能。

程式碼就變成這樣了。

func postParamsToQuery(r *http.Request) url.Values {
  body := io.ReadAll(r.Body)
  post, _ := url.ParseQuery(body) // 解析錯誤按空引數處理
  return trimMapValues(post)
}

五、另外一個邏輯缺陷

其實,上面的 POST 程式碼還有一個邏輯缺陷。

對於一個 POST 請求,PATH 上的引數需要進行 GET 獲取的,BODY 裡的引數才需要 POST 獲取。

業務極有可能把固定的引數放在 PATH 中,變化的引數放在 BODY 中。

比如下面的樣子

GET /path?otype=json HTTP/1.1
Host: github.tiankonguse.com


k1=v1&k2=v2

此時,我們應該把 GET 引數與 POST 引數組合起來才行。

大概程式碼如下

func postParamsToQuery(r *http.Request) url.Values {
  query := r.URL.Query()  // 獲取 GET 的 Kv
  body := io.ReadAll(r.Body)
  post, _ := url.ParseQuery(body) // 解析錯誤按空引數處理
  queryAndPost = merge(query, post) // 合併 GET 與 POST
  return trimMapValues(queryAndPost)
}

六、複用 Http 庫

當然,上面的程式碼我並沒有實現。

因為我馬上猜想,這個邏輯 go 的 HTTP 庫應該都封裝好了的。

於是我又閱讀了 HTTP 庫的 Request 檔案的全部原始碼,發現果然已經封裝好了。

於是程式碼可以簡化為下面的樣子了。

func postParamsToQuery(r *http.Request) url.Values {
  r.ParseForm() // HTTP Request 自動合併 GET/POST 到 Form
  return trimMapValues(r.Form)
}

七、繼續優化

還是看最初的程式碼。

這裡 30 多行程式碼,都是為了進行 GET 和 POST 請求的引數合併。

既然現在我們知道 HTTP 庫的 Request 庫會幫我們做這件事,我們就可以把這些程式碼都刪除了,保留一個函式就行了。

func trimMapValues(query url.Values) url.Values {
  for _, v := range query {
    if len(v) > 0 {  // 這裡需要判斷長度,小夥伴也沒判斷
      v[0] = strings.TrimSpace(v[0])
    }
  }
  return query
}
func getQuery(r *http.Request) url.Values {
  r.ParseForm() // HTTP Request 自動合併 GET/POST 到 Form
  return trimMapValues(r.Form)
}

八、最後

對於 trimMapValues 這個函式,本來不應該做這個邏輯的,這樣寫非常不優雅。

但是舊的 CGI 框架自動做了這個邏輯。

根據墨菲定律,如果一個事情有機率發生,那最終肯定會發生。

當時灰度上線的時候,發現確實有個別業務在引數的前後加了空格(就是這麼神奇)。

那隻能保留這個歷史包袱了。

好了,這是目前我想到的方案,30 多行程式碼優化到 10 行左右。

請教下,同時支援 GET 和 POST 請求,你有什麼建議嗎?

《完》

-EOF-

本文公眾號:天空的程式碼世界

個人微訊號:tiankonguse

QQ演算法群:165531769(不止演算法)

知識星球:不止演算法