不看就落后了,Go 1.22 中更好的http router

很多人为了使用Go web中更好的路由,会使用第三方的库 httproutergorilla/mux等。在明年的春节左右发布的Go 1.22中,Go官方终于对标准库中的http.ServeMux下手了,对它的功能进行了优化,终于可以抛弃第三方库了。

一个令人兴奋的提案预计将在Go 1.22中实现—— 增强标准库net/http包中默认HTTP服务多路复用器的模式匹配能力。

现有的多路复用器(http.ServeMux)提供了基本的路径匹配,但除此之外没有太多。这导致了一堆的第三方库实现了更强大的功能。

Go 1.22中的新多路复用器将通过提供高级匹配能力来显著弥合与第三方库的差距。在这篇短文中,我将快速介绍新的多路复用器(mux)。我还将给出REST服务器的示例,并比较新的标准库muxgorilla/mux的性能。

使用新的mux

如果你曾经在Go中使用过第三方 mux/router(比如gorilla/mux),那么使用新的标准mux将是简单而熟悉的。从阅读它的文档开始——它简短丝滑。

让我们来看几个基本用法示例。我们的第一个示例演示了mux的一些新模式匹配功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 method(在本例中为GET)被明确指定为模式的一部分。这意味着该处理程序将只触发以/path/开头的路径的GET请求,而不触发其他HTTP method。
  • 在第二个处理程序中,第二个路径组件 - {id}中有一个通配符,这是以前不支持的。通配符将匹配单个路径组件,然后处理程序可以通过请求的PathValue方法访问匹配的值。

由于Go 1.22尚未发布,你可以使用gotip运行此示例。请参阅完整的代码示例以及运行此程序的完整说明。让我们来试试这个服务器:

1
$ gotip run sample.go

在一个单独的终端中,我们可以发出一些curl调用来测试它:

1
2
3
4
5
6
7
8
9
10
11
$ 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通配符是如何被分配一个值的。我再次鼓励您查看新ServeMux的文档。您将了解其他功能,如将尾随路径与带有{id}的通配符匹配,路径以{$}结尾的严格匹配以及其他规则。

提案中特别注意不同模式之间的潜在冲突。请考虑此设置:

1
2
3
4
5
6
7
8
9
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。事实上,对于上面的例子,我们得到了如下内容:

1
2
3
4
5
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实现服务器

REST Servers in Go series的REST服务器使用几种不同的方法为Go中的任务/待办事项列表应用程序实现了一个简单的服务器。第1部分从标准库开始,第2部分使用gorilla/mux路由器重新实现了相同的服务器。

现在是再次实现它的好时机,但有了Go 1.22的增强mux;将该解决方案与使用gorilla/mux的解决方案进行比较将特别有趣。

此项目的完整代码可在此处获得。让我们看看几个有代表性的代码示例,从模式注册开始:

1
2
3
4
5
6
7
8
9
10
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 method将请求(具有相同路径)路由到不同的处理程序;使用旧的http.ServeMux这样的匹配器就必须转到同一个处理程序,然后由该处理程序根据该方法决定要做什么。

让我们看看其中一个处理程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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返回的错误。

总之,最终结果与第2部分中使用gorilla/mux的解决方案非常相似。与普通的标准库方法相比,处理程序的方式要好得多,因为mux现在可以进行更复杂的路由,而不会将许多路由决策留给处理程序本身。

结论

“我应该使用哪个router库?”一直是Go初学者的常见问题。我相信在Go 1.22发布后,这个问题的常见答案会发生变化,因为许多人会发现新的标准库 mux足以满足他们的需求,而无需求助于第三方软件包。

其他人会坚持熟悉的第三方库,这完全没关系。像gorilla/mux这样的路由器仍然提供比标准库更多的功能;除此之外,许多Go程序员选择了像Gin这样的轻量级框架,它提供了一个路由器,但也提供了用于构建web后端的额外工具。

总而言之,这对所有Go用户来说无疑是一个积极的变化。无论人们是使用第三方软件包还是坚持使用标准库,让标准库更有能力对整个社区来说都是一个积极的方面。

翻译自Better HTTP server routing in Go 1.22