請教下,go HTTP 服務如何同時支援 GET 與 POST
一、背景
專案裡有個讀介面是 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(不止演算法)
知識星球:不止演算法
- leetcode 第 312 場演算法比賽
- leetcode 第 311 場演算法比賽
- leetcode 第 310 場演算法比賽
- leetcode 第 286 場演算法比賽
- 請教下,go HTTP 服務如何同時支援 GET 與 POST
- 設計模式之迭代器模式
- 佇列卡住?方向錯了,使出十八般武藝也沒用
- 兩臺電腦如何複製資料,同事的方法我震驚了
- 影片的位元率、關鍵幀、FPS啥意思?
- Leetcode 第 171 場比賽回顧
- TCP服務端主動斷開連線問題
- Leetcode 第 179 場比賽回顧
- 觀看得到與B站的跨年晚會
- Leetcode 第 169 場比賽回顧
- Leetcode 第 168 場比賽回顧
- Leetcode 第 166 場比賽回顧
- Leetcode 第 165 場比賽回顧
- Leetcode 第 164 場比賽回顧
- 開始使用 vscode 了
- Leetcode 第 161 場比賽回顧