在Go 1.22中,更好的HTTP服务器路由
Better HTTP server routing in Go 1.22

原始链接: https://eli.thegreenplace.net/2023/better-http-server-routing-in-go-122/

Go 1.22通过新的http.ServeMUX在标准库mux中引入了高级模式匹配能力,这允许在不需要第三方包的情况下进行更具体的路由。使用通配符(如{id})允许在任务列表应用中对个体任务进行动态路由。模式注册优先级高于潜在冲突,以避免复杂注册方案中的问题。以前,标准库解决方案要求每个处理函数匹配特定路径,需要所有HTTP方法。然而,随着新mux的出现,特定HTTP方法可以触发特定处理函数,而不是依赖于每个函数内的方法检测。虽然路由器(如gorilla/mux)继续提供附加功能,但标准库的增强为Go用户群带来了更多好处。在线提供了完整的代码示例以演示实现。提供电子邮件联系信息以便反馈。 总之,这些改进为Go开发者提供了更大的灵活性和便利性,而无需外部依赖。此外,改进的模式匹配能力使创建更直观和强大的RESTful API变得更加容易。随着Go越来越受欢迎,将此类增强添加到核心语言进一步巩固了其作为编程语言中实用选择的名声。

总的来说,这次讨论中提出的第二个代码片段的主要批评包括可读性差、处理路由的灵活性不足、潜在的技术债务、扩展到复杂网站的困难、缺乏明确的返回类型以及过于冗长的倾向。一些用户推荐的替代方案是声明式路由、自动输入验证、使用中间件进行输入验证和身份验证,以及基于类的控制器。其他用户评论说,在express.js普及之前,诸如Sinatra和NancyFX(C#)等框架具有类似的语法,但现代替代品如NancyFX或ASP.NET Core正逐渐摆脱这些较旧的语法。然而,用户之间的意见可能有所不同。
相关文章

原文

An exciting proposal is expected to land in Go 1.22 - enhancing the pattern-matching capabilities of the default HTTP serving multiplexer in the net/http package.

The existing multiplexer (http.ServeMux) offers rudimentary path matching, but not much beyond that. This led to a cottage industry of 3rd party libraries to implement more powerful capabilities. I've explored these options in my REST Servers in Go series, in parts 1 and 2.

The new multiplexer in 1.22 is going to significantly bridge the gap from 3rd party packages by providing advanced matching. In this short post I'll provide a quick introduction to the new multiplexer (mux). I'll also revisit the example from the REST Servers in Go series and compare how the new stdlib mux fares against gorilla/mux.

A cartoony go gopher holding a multiplexer

Using the new mux

If you've ever used a 3rd party mux / router package for Go (like gorilla/mux), using the new standard mux is going to be straightforward and familiar. Start by reading its documentation - it's short and sweet.

Let's look at a couple of basic usage examples. Our first example demonstrates some of the new pattern matching capabilities of the 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)
}

Experienced Go programmers will notice two new features right away:

  1. In the first handler, the HTTP method (GET in this case) is specified explicitly as part of the pattern. This means that this handler will only trigger for GET requests to paths beginning with /path/, not for other HTTP methods.
  2. In the second handler, there's a wildcard in the second path component - {id}, something that wasn't supported before. The wildcard will match a single path component and the handler can then access the matched value through the PathValue method of the request.

Since Go 1.22 hasn't been released yet, I recommend running this sample with gotip. Please see the complete code sample with full instructions for running this. Let's take this server for a ride:

And in a separate terminal we can issue some curl calls to test it:

$ 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

Note how the server rejects a POST request to /path/, while the (default for curl) GET request is allowed. Note also how the id wildcard gets assigned a value when the request matches. Once again, I encourage you to review the documentation of the new ServeMux. You'll learn about additional capabilities like matching trailing paths to a wildcard with {id}..., strict matching of a path end with {$}, and other rules.

Particular care in the proposal was given to potential conflicts between different patterns. Consider this setup:

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)
})

And suppose the server receives a request for /task/0/status/ -- which handler should it go to? It matches both! Therefore, the new ServeMux documentation meticulously describes the precedence rules for patterns, along with potential conflicts. In case of a conflict, the registration panics. Indeed, for the example above we get something like:

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.

The message is detailed and helpful. If we encounter conflicts in complex registration schemes (especially when patterns are registered in multiple places in the source code), such details will be much appreciated.

Redoing my task server with the new mux

The REST Servers in Go series implements a simple server for a task/todo-list application in Go, using several different approaches. Part 1 starts with a "vanilla" standard library approach, and Part 2 reimplements the same server using the gorilla/mux router.

Now is a great time to reimplement it once again, but with the enhanced mux from Go 1.22; it will be particularly interesting to compare the solution to the one using gorilla/mux.

The full code for this project is available here. Let's look at a few representative code samples, starting with the pattern registration :

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)

Just like in the gorilla/mux sample, here we use specific HTTP methods to route requests (with the same path) to different handlers; with the older http.ServeMux, such matchers had to go to the same handler, which would then decide what to do based on the method.

Let's also look at one of the handlers:

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)
}

It extracts the ID value from req.PathValue("id"), similarly to the Gorilla approach; however, since we don't have a regexp specifying that {id} only matches integers, we have to pay attention to errors returned from strconv.Atoi.

All and all, the end result is remarkably similar to the solution that uses gorilla/mux from part 2. The handlers are much better separated than in the vanilla stdlib approach, because the mux now can do much more sophisticated routing, without leaving many of the routing decisions to the handlers themselves.

Conclusion

"Which router package should I use?" has always been a FAQ for beginner Go programmers. I believe the common answers to this question will shift after Go 1.22 is released, as many will find the new stdlib mux sufficient for their needs without resorting to 3rd party packages.

Others will stick to familiar 3rd party packages, and that's totally fine. Routers like gorilla/mux still provide more capabilities than the standard library; on top of it, many Go programmers opt for lightweight frameworks like Gin, which provide a router but also additional tools for building web backends.

All in all, this is certainly a positive change for all Go users. Making the standard library more capable is a net positive for the entire community, whether people use 3rd party packages or stick to just the standard library.


联系我们 contact @ memedata.com