Go 1.22 中更优秀的 HTTP 服务器路由
- Published on
在 Go 1.22 中,一个令人兴奋的提案即将落地 —— 增强默认 HTTP 服务多路复用器(net/http
包)的模式匹配能力。
目前的多路复用器 (http.ServeMux
) 仅提供基本的路径匹配,功能相当有限。这导致了大量第三方库的出现,以实现更强大的路由功能。
1.22 中的新多路复用器将通过提供高级匹配功能,显著缩小与第三方包的差距。在这篇短文中,我将简要介绍新的多路复用器(mux),并将对比新的标准库 mux 与 gorilla/mux
。
使用新的 mux
如果您之前使用过 Go 的第三方 mux/路由器包(如 gorilla/mux
),那么使用新的标准 mux 将会非常熟悉和直观。建议先阅读其文档 - 文档简短精炼。
让我们看几个基本使用示例。第一个示例展示了新 mux 的模式匹配能力:
package main
import (
"fmt"
"net/http"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /path/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "got path\n")
})
mux.HandleFunc("/task/{id}/", func(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
fmt.Fprintf(w, "handling task with id=%v\n", id)
})
http.ListenAndServe("localhost:8090", mux)
}
经验丰富的 Go 程序员会立即注意到两个新特性:
在第一个处理器中,HTTP 方法(这里是
GET
)被明确指定为模式的一部分。这意味着该处理器只会触发/path/
开头的 GET 请求,而不会响应其他 HTTP 方法。在第二个处理器中,路径的第二个组件有一个通配符 -
{id}
,这是之前版本不支持的。通配符将匹配单个路径组件,处理器可以通过PathValue
方法获取匹配的值。
建议使用 gotip
运行此示例。让我们测试这个服务器:
$ gotip run sample.go
在另一个终端中,我们可以发送一些 curl 请求进行测试:
$ curl localhost:8090/what/
404 page not found
$ curl localhost:8090/path/
got path
$ curl -X POST localhost:8090/path/
Method Not Allowed
$ curl localhost:8090/task/f0cd2e/
handling task with id=f0cd2e
注意服务器如何拒绝对 /path/
的 POST 请求,而允许(curl 默认的)GET 请求。另外,当请求匹配时,id
通配符会被赋值。如使用 {id}...
匹配尾随路径的通配符,使用 {$}
严格匹配路径末尾等规则。
在提案中,对不同模式之间潜在的冲突给予了特别的关注。考虑以下设置:
mux := http.NewServeMux()
mux.HandleFunc("/task/{id}/status/", func(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
fmt.Fprintf(w, "handling task status with id=%v\n", id)
})
mux.HandleFunc("/task/0/{action}/", func(w http.ResponseWriter, r *http.Request) {
action := r.PathValue("action")
fmt.Fprintf(w, "handling task 0 with action=%v\n", action)
})
假设服务器收到 /task/0/status/
的请求 - 哪个处理器应该处理?它同时匹配这两个!因此,新的 ServeMux
文档详细描述了模式的优先级规则,并说明了潜在的冲突。如果存在冲突,注册将会引发 panic。对于上面的示例,我们会得到类似这样的错误:
panic: pattern "/task/0/{action}/" (registered at sample-conflict.go:14) conflicts with pattern "/task/{id}/status/" (registered at sample-conflict.go:10):
/task/0/{action}/ and /task/{id}/status/ both match some paths, like "/task/0/status/".
But neither is more specific than the other.
/task/0/{action}/ matches "/task/0/action/", but /task/{id}/status/ doesn't.
/task/{id}/status/ matches "/task/id/status/", but /task/0/{action}/ doesn't.
这个错误消息详细且有帮助。如果我们在复杂的注册方案中遇到冲突(尤其是在源代码的多个位置注册模式),这样的细节将会非常受欢迎。
使用新的 mux 重新实现任务服务器
让我们看几个代表性的代码示例,首先是路由注册:
mux := http.NewServeMux()
server := NewTaskServer()
mux.HandleFunc("POST /task/", server.createTaskHandler)
mux.HandleFunc("GET /task/", server.getAllTasksHandler)
mux.HandleFunc("DELETE /task/", server.deleteAllTasksHandler)
mux.HandleFunc("GET /task/{id}/", server.getTaskHandler)
mux.HandleFunc("DELETE /task/{id}/", server.deleteTaskHandler)
mux.HandleFunc("GET /tag/{tag}/", server.tagHandler)
mux.HandleFunc("GET /due/{year}/{month}/{day}/", server.dueHandler)
就像在 gorilla/mux
示例中一样,这里我们使用特定的 HTTP 方法来将相同路径的请求路由到不同的处理器;在旧版 http.ServeMux
中,这些匹配必须转到同一个处理器,然后根据方法决定要做什么。
让我们看一个处理器的示例:
func (ts *taskServer) getTaskHandler(w http.ResponseWriter, req *http.Request) {
log.Printf("handling get task at %s\n", req.URL.Path)
id, err := strconv.Atoi(req.PathValue("id"))
if err != nil {
http.Error(w, "invalid id", http.StatusBadRequest)
return
}
task, err := ts.store.GetTask(id)
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
renderJSON(w, task)
}
它通过 req.PathValue("id")
提取 ID 值,类似于 Gorilla 的方法;然而,由于没有正则表达式指定 {id}
只匹配整数,我们必须注意 strconv.Atoi
返回的错误。
总的来说,处理器的分离比原生标准库方法更好,因为现在的 mux 可以进行更复杂的路由,而无需将太多路由决策留给处理器本身。
结论
对于初学的 Go 程序员来说,"我应该使用哪个路由器包?"一直是一个常见问题。我认为,在 Go 1.22 发布后,常见的回答将会发生变化,许多开发者会发现新的标准库 mux 已经足够满足需求,无需求助于第三方包。
部分开发者可能会继续使用熟悉的第三方包,这也完全没问题。像 gorilla/mux
这样的路由器仍然提供比标准库更多的功能。此外,许多 Go 程序员会选择轻量级框架,如 Gin,它们不仅提供路由器,还提供构建 Web 后端的其他工具。
总的来说,这对所有 Go 用户来说都是一个积极的变化。让标准库更加强大,对整个社区来说是一个净正面的改进,无论人们是使用第三方包还是仅依赖标准库。