Go 從 Golang 1.22 routing enhancement 看專案開發

這次 Golang 1.22 版本針對內建的 http.ServeMux (HTTP request multiplexer) 進行改善,原本 http.ServeMux 的設計力求直覺單純,適用在架設簡單 HTTP server 的應用場景;但隨著 RESTful API 成為目前 HTTP-based interface 設計主流,而既有的 http.ServeMux 並沒有辦法實作 RESTful API,導致內建的 http.ServeMux 使用時機愈來愈少,Gopher 必須仰賴第三方套件才能實現 RESTful API。為了改善此問題,Golang 1.22 加強了 http.ServeMux 的 routing 設計,除了能夠辨別 HTTP method 之外,也增加了動態路徑 wildcard 的功能,使 http.ServeMux 能夠滿足實務上基本 HTTP server 的需求。

如果單純從開發角度來看,修改 http.ServeMux 的 routing 實作看起來不會是個太困難的任務,但是考慮到既有用戶相容、設計理念、路徑判斷機制等,原本看似簡單的項目,實際上也需要各方面的考量。所以這篇文章主要在彙整 proposal issue 上的想法和內容,讓我們可以進一步思考,這些討論方向和思考面向能否應用在工作的專案中,使得專案在開發時可以更加完善。

Routing Enhancements for Go 1.22

提案和動機

📖 We would like to expand the standard HTTP mux's capabilities by adding two features:
1. distinguishing requests based on HTTP method (GET, POST, ...)
2. support for wildcards in the matched paths.
Both features are particularly important to REST API servers.

Adding these features to the standard library means one fewer dependency for many projects.

每項提案背後都必須要有一個明確的動機,正如一開始所提及,過往的 http.ServeMux 功能過於陽春,在 RESTful API 概念普及的情況下,內建的 http.ServeMux 實際能應用在專案上的範圍很有限,無法支援常見的 CRUD 場景(畢竟現在談到 CRUD 就很自然地聯想到結合 GET, POST, PUT 等 HTTP method 所產生的 endpoint),頂多用在教學、提供 metrics、或是 health 等單一用途的時候。

1// before go 1.22
2// requests with /health path will route to it regardless of the HTTP method
3http.HandleFunc("/health", func(resp http.ResponseWriter, req *http.Request) {
4  fmt.Fprintf(resp, "healthy")
5	})

因此,提案的內容就是新增 HTTP method 的判斷和在路徑中增加 wildcard 以支援 path parameter 設定,如此一來就能符合實作 RESTful API 的基本需求。

1// go 1.22
2// requests with GET method and /user/* path will route to it
3http.HandleFunc("GET /user/{name}", func(resp http.ResponseWriter, req *http.Request) {
4    param := r.PathValue("name")
5	  fmt.Fprintf(resp, "user name is %s", name)
6	})

設計理念

提案中的兩項功能,表面上看起來很簡單,但是實際上卻是有著不同面向的考量,這也是我覺得很有意思的地方。其中一個就是貫徹 http.ServeMux 原先的設計理念:

📖 It remains a key design goal to avoid any semantics that depend on registration order.

當我們在使用第三方 HTTP routing 或是 web framework,有時候會發現 matched 到的 handler 會基於 handler 的註冊順序,例如以下例子:

  1. /news/{news_id}
  2. /news/latest

假設 framework 允許這樣註冊行為,那麼先註冊第一個再註冊二個,有可能就會導致 /news/* 的 request 都走第一個註冊的 handler 而不會走到第二個。如果使用者希望 /news/latest 的 request 能正確走到第二個 handler,那就必須要自己調整註冊順序,先註冊第二個再註冊第一個,這樣在 match 不到 /news/latest 的 request 才會被導向 /news/{news_id}

而 Golang http.ServeMux 的設計方向是希望能夠不要依賴於註冊順序,避免造成以上的錯誤判斷。這次的改善提案中也是基於這樣的理念來進行功能擴展。

設計細節:Routing 的規則

提案中希望 path 能支援 wildcard 的功能,那麼 routing 規則該如何基於設計理念並且結合 wildcard 來調整,就會是這個功能的重點實作細節。他們討論了幾個 router 常見的實作方式:

  1. disallow overlaps
  2. the pattern that was registered last
  3. longest literal prefix

在綜合考量之下,決定採用 the most specific pattern wins 的規則,例如:

  1. /users/{u}/posts/latest (preferred)
  2. /users/{u}/posts/{id}

路徑中 /users/{u}/posts/latest 比起 /users/{u}/posts/{id} 的 match 範圍更小,所以會被優先選擇,無關於這兩個規則的註冊順序

  1. GET /posts/{id} (preferred)
  2. /posts/{id}

另一個例子,這兩個規則都能被註冊,但是 1 因為有限定 GET HTTP method,其 match 範圍比 2 小,所以也會被優先選擇。同樣地,無關於這兩個規則的註冊順序

Proposal Issue 中有繪製出很清楚的圖例,說明哪些規則會被選擇。而如果在註冊時出現規則衝突,則會直接 panic 出錯,避免因為註冊順序的差異而造成不如預期的路徑判斷。

實作

討論出設計細節後,就能落實實作了,實作的 source code 並不複雜,把 pattern 拆解成各 segment 並且逐一進行比較,來確認 pattern 之間是否有衝突。

而在 request routing 的時候,基於 the most specific pattern wins 的原則,會先找出範圍小的規則來進行比對,如果不 match 則會透過 backtracking 演算法,找到上一層範圍比較大的規則再進行比對。舉例來說,router 中有兩個規則:

  1. /users/{u}/posts/latest
  2. /users/{u}/posts/{id}

當 request /users/cherie/posts/1 要進行 match 的時候,則第一個規則會先比對,發現不符合後,再比對第二個規則。

影響

雖然功能擴展提高了實用性,不過使用者也會想知道新增這兩項功能所造成的影響。提案者有特別提及到使用者所關心的效能議題,其中效能分為兩部分:

  1. request 進來時 match 的效率
  2. server 在一開始啟動時註冊 handler,註冊過程中發現規則衝突的時間
📖 The reference implementation for this proposal matches requests about as fast as the current ServeMux on Julien Schmidt’s static benchmark.

針對第一個效能問題,提案者說明 match 效率跟既有的版本基本上一致,而他也認為 match 時間在他看起來不會是重要的問題(畢竟改善初衷是想要讓更多專案能使用內建 library,而這些專案可能只有註冊為數不多的 handler,自然也就不會佔據太多 routing 時間)。

📖 Detecting conflicts when a pattern is registered seems to require checking all previously registered patterns in general.
📖 Indexing the patterns as they are registered can significantly speed up the common case.

另外,針對第二個問題,他們在實作上也有此考量,因此有 indexing 的設計來加快比對的效率。不過討論上也有提及到,實務上還是需要收集案例進行效能實驗才能證實。

結語

從這個功能中可以看到蠻完整的專案流程,從一開始的動機、功能需求、提出功能設計方向並比較多個實作方案來選擇適合專案的方案,還有說明可能造成的影響。在討論過程中緊扣著設計初衷,包含不依賴註冊順序、提高實用性為主而不追求效能改善,是我覺得很不錯的地方。雖然最後成果可能不是每個使用者都覺得很棒 XD 畢竟要使用者寫成特定的格式 [METHOD] [HOST]/[PATH] 才可以正確執行,但過程中的討論和發想還是值得學習的。