Golang 作為一個偏向 server 應用的程式語言,一般的 web server 並不會直接使用原生的 package net/http
,而更多的使用 gin-gonic/gin
或是 gorilla/mux
,後來也有 labstack/echo
以及 go-chi/chi
等等選擇,在效能、輕量、好維護、好擴充中,都能找到對應的 third party package,其中的原因不外乎是原生的 package 提供的功能過於簡潔。
好在 1.22 中,官方改進了 net/http
中對於多工器、路由,甚至出了一篇部落格,現在更可以「大膽的」直接使用 standard library。
Path Parameter
若要將應用的 Web API 定義成 RESTful,我們會使用 /資源/{資源唯一識別符}/子資源/{子資源唯一識別符}
來定義路徑。假如要獲取一個使用者的訂單,則會使用 GET /users/1/orders
來獲取。在 1.22 以前,我們只能定義到 /users
,再自行解析往後的 path:
http.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
subPath := strings.TrimPrefix(req.URL.Path, "/users/")
if len(subPath) == 0 {
xxx
} else {
ooo
}
...
})
而在 1.22 中新增了 net.http
對 path parameter 的支持,我們可以直接使用 (*http.Request).PathValue("xxx")
來獲取:
http.HandleFunc("/users/{user_id}", func(w http.ResponseWriter, r *http.Request) {
userID := r.PathValue("user_id")
...
})
不過也帶來一些限制
相容 go 1.x
眾所周知 Golang 是一個極度在意簡單性、向後相容的程式語言,為了不要因為升到 1.22 而發生非預期的錯誤,是可以讓 path parameter 的路由與一般路由並存的。
The precedence rule is simple: the most specific pattern wins. This rule matches our intuition that posts/latests should be preferred to posts/{id}, and /users/{u}/posts/latest should be preferred to /users/{u}/posts/{id}. It also makes sense for methods. For example, GET /posts/{id} takes precedence over /posts/{id} because the first only matches GET and HEAD requests, while the second matches requests with any method.
舉例來說:
mux.HandleFunc("/orders/{order_id}", xxx)
mux.HandleFunc("/orders/latest",xxx)
若是使用 gin 的話,這種路由註冊將在 runtime 出現 panic,也是由於 gin 是一個基於 valyala/fasthttp
的 package,而 valyala/fasthttp
又是基於 radix 這種資料結構,node 間發生了衝突才引發 panic。
net/http
則是使其相容於舊版本:
只有在發生模糊不清的路徑時,才會在 runtime 發生 panic:
mux.HandleFunc("/orders/latest",xxx)
mux.HandleFunc("/{other_resource}/latest")
// pattern "/{other_resource}/latest" (registered at /home/raiven/go-http-22/main.go:110) conflicts with pattern "/orders/{order_id}"
更進一步的相容可以打開 GODEBUG=httpmuxgo121=1
來使其單獨 rollback 回 1.21。
package http
type ServeMux struct {
mu sync.RWMutex
tree routingNode
index routingIndex
patterns []*pattern // TODO(jba): remove if possible
mux121 serveMux121 // used only when GODEBUG=httpmuxgo121=1
}
指定 Method
可以直接將 http method 寫在路由判斷內,這個改動相較簡單,卻又能大幅度的減少程式碼,直接寫範例:
before upgrading to 1.22:
mux.HandleFunc("/orders", func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
xxx
} else if r.Method == http.MethodPost {
xxx
} else {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
})
after upgrading to 1.22:
mux.HandleFunc("GET /orders", func(w http.ResponseWriter, r *http.Request) {})
mux.HandleFunc("POST /orders", func(w http.ResponseWriter, r *http.Request) {})
比較編譯大小
如此一來有一些很小的 package 就不在需要引入碩大的 third party package,比較一下不同的 package 在 handle localhost:8080/hello
的 binary 大小:
全部基於 linux,amd64,go1.22.0
net/http
package main
import "net/http"
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /hello", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("Hello, World!"))
})
http.ListenAndServe(":8080", mux)
}
gin-gonic/gin
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.GET("/hello", func(c *gin.Context) {
c.JSON(http.StatusOK, "Hello, World!")
})
http.ListenAndServe(":8080", router)
}
go-chi/chi
package main
import (
"net/http"
"github.com/go-chi/chi/v5"
)
func main() {
r := chi.NewRouter()
r.Get("/hello", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("Hello, World!"))
})
http.ListenAndServe(":8080", r)
}
gorilla/mux
package main
import (
"net/http"
"github.com/gorilla/mux"
)
func main() {
r := mux.NewRouter()
r.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("Hello, World!"))
})
http.ListenAndServe(":8080", r)
}
labstack/echo
package main
import (
"net/http"
"github.com/labstack/echo/v4"
)
func main() {
e := echo.New()
e.GET("/hello", func(c echo.Context) error {
return c.String(http.StatusOK, "Hello, World!")
})
e.Logger.Fatal(e.Start(":8080"))
}
使用 go build main.go
來編譯成可執行檔,並使用 du -sh main
來查看執行檔大小:
package | size |
---|---|
net/http | 6.8M |
gin-gonic/gin | 11M |
go-chi/chi | 7.1M |
gorilla/mux | 7.1M |
labstack/echo | 7.5M |
假如自己的 side project 每天都要編譯一個 nightly version 的 docker image,使用 gin 將比原生的 net/http 多出 1.5G 的存儲空間。