自定义路由设计
本文主要讲解go语言web编程中自定义路由器的设计。在此之前需要先了解一下go语言web编程中路由与http服务的基本原理,可以参考笔者另一篇博文:go web编程——路由与http服务 。
我们已经知道,go的默认路由器只支持路由绝对匹配,无法支持正则匹配,这样就没办法设计一些简洁、优雅的路由。那怎么让路由支持正则匹配呢?通过阅读源码,可以发现http服务器和路由器之间是解耦的,调用 http.ListenAndServe(addr string, handler Handler) 方法启动http服务的时候,若第二个参数指定为 nil ,则会使用go的默认路由器,否则使用这个参数指定的路由器。所以,我们可以设计一个支持正则匹配的路由器,然后在第二个参数指定为我们自定义的路由器。
首先,我们要知道我们要设计的这个路由器需要符合什么条件,才能赋值给 http.ListenAndServe(addr string, handler Handler) 方法中的第二个参数。可以看到它的类型是 Handler ,而 Handler 是一个接口类型,源码如下:
1 2 3 | type Handler interface { ServeHTTP(ResponseWriter, *Request) } |
所以我们自定义的路由器也需要实现这个接口。事实上go默认的路由器类型 ServeMux 也实现了这个接口,http服务处理客户端请求时会调用路由器的方法 ServeHTTP(ResponseWriter, *Request) 进行路由匹配和请求分发。这是http服务器和路由器唯一有交集的地方,所以只要我们自定义的路由器也定义方法 ServeHTTP(ResponseWriter, *Request) 并在其中实现我们自定义的路由匹配和请求分发,就完全可以使用我们自定义的路由器取代go默认的路由器,至于路由器结构怎么设计,路由怎么注册,又怎么匹配,都是我们说了算。
接下来先写一个简单的自定义路由器,取代go默认路由器,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | package main import ( "log" "net/http" ) //路由器结构 type SimServeMux struct { } //路由匹配与请求分发 func (mux *SimServeMux) ServeHTTP(writer http.ResponseWriter, req *http.Request) { if req.URL.Path == "/" { sayHello(writer, req) return } else if req.URL.Path == "/haha" { haha(writer, req) return } http.NotFound(writer, req) } func main() { log.Println( "server running..." ) log.Fatal(http.ListenAndServe( "localhost:4001" , &SimServeMux{})) } func sayHello(writer http.ResponseWriter, req *http.Request) { writer.Write([]byte( "hello world!" )) } func haha(writer http.ResponseWriter, req *http.Request) { writer.Write([]byte( "haha!" )) } |
编译,运行,浏览器访问 http://localhost:4001/ ,输出 hello world! ,访问http://localhost:4001/haha ,输出 haha! 。
可以看到,我们自定义的路由器已经取代go的默认路由器了。这是一个极其简单的路由器,只是定义了 ServeHTTP(ResponseWriter, *Request) 方法来进行路由匹配。
言归正传,接下来我们就来设计一个满足RESTful规则,而且支持正则匹配的路由器。
首先考虑路由器的结构,由于需要满足RESTful规则,可以使用一个 map 来存储注册路由,这个 map 的键为请求方法的名称,例如GET、POST等,map 的值则是路由结构体的切片(路由结构暂时未知)。这样做的好处是,进行路由匹配的时候,可以先根据请求方法过滤掉其他请求方法的路由,只需遍历当前请求方法的路由结构体切片进行匹配即可。动态路由使用正则匹配,而静态路由比较简单,使用请求文件路径前缀匹配即可。考虑到并发请求,再加一个读写锁,所以路由器结构如下:
1 2 3 4 5 6 | //路由器结构 type simMux struct { mu sync.RWMutex //读写锁 m map [string][]HandlerStruc //动态路由 static map [string]string //静态路由 } |
那么路由结构体的结构又应该是怎么样的呢?路由结构体除了保存响应函数,还需要保存一些额外的信息。假设我们要使用的正则路由规则形如 /user/:id([0-9]+)/:name([a-z]+) ,可以匹配的具体路由则形如 /user/666/shiajun ,那么我们在注册路由的时候,需要把正则表达式 /user/([0-9]+)/([a-z]+) 和请求参数名称 id、name 保存起来,而且要明确记录 id 是第一个参数,name 是第二个参数,这样路由匹配时对于请求路径 /user/666/shiajun 才知道 id 的值为666,name 的值为 shiajun ,所以路由结构体的结构如下:
1 2 3 4 5 6 7 | //路由结构 type HandlerStruc struct { regex *regexp.Regexp //正则对象 params map [int]string //请求参数 handler Handler //响应函数 } type Handler func (http.ResponseWriter, *http.Request) |
创建路由器对象的方法:
1 2 3 4 5 6 7 8 | //创建路由器 func New() *simMux { return &simMux{ sync.RWMutex{}, make( map [string][]HandlerStruc), make( map [string]string), } } |
接下来就要实现一个注册路由的方法,注册路由的过程其实上面已经都解释得很清楚了,直接上代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | //注册静态路由 func (sMux *simMux) AddStatic(prefix, path string) bool { if len(prefix) == 0 || len(path) == 0 { return false } _, exist := sMux.static[prefix] if exist { panic( "simMux: duplicate static prefix \"" + prefix + "\"" ) } sMux.static[prefix] = path return true } //注册动态路由 //支持路由正则匹配,格式:/user/:id([0-9]+)/:name([a-z]+) func (sMux *simMux) add(method string, pattern string, handler Handler) bool { if len(pattern) == 0 || handler == nil { return false } sMux.mu.Lock() defer sMux.mu.Unlock() params := make( map [int]string) //请求参数 var patterns []string //正则表达式组成 pos := 0 arr := strings.Split(pattern, "/" ) for _, v := range arr { if strings.HasPrefix(v, ":" ) { index := strings.Index(v, "(" ) if index != -1 { patterns = append(patterns, v[index:]) params[pos] = v[1:index] pos++ continue } } patterns = append(patterns, v) } regex, err := regexp.Compile(strings.Join(patterns, "/" )) if err != nil { panic( "simMux: wrong pattern \"" + pattern + "\"" ) } sMux.m[method] = append(sMux.m[method], HandlerStruc{regex, params, handler}) return true } |
然后封装各个请求方法的路由注册方法,方便调用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | //注册GET方法路由 func (sMux *simMux) AddGet(pattern string, handler Handler) bool { return sMux.add(http.MethodGet, pattern, handler) } //注册POST方法路由 func (sMux *simMux) AddPost(pattern string, handler Handler) bool { return sMux.add(http.MethodPost, pattern, handler) } //注册PUT方法路由 func (sMux *simMux) AddPut(pattern string, handler Handler) bool { return sMux.add(http.MethodPut, pattern, handler) } //注册DELETE方法路由 func (sMux *simMux) AddDelete(pattern string, handler Handler) bool { return sMux.add(http.MethodDelete, pattern, handler) } |
最后就是路由匹配与请求分发的实现了。根据请求方法定位到具体路由结构体切片,遍历切片进行正则匹配,若匹配成功,拼接请求参数,调用对应响应函数,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | //路由匹配与请求分发 func (sMux *simMux) ServeHTTP(writer http.ResponseWriter, req *http.Request) { //静态路由解析 for prefix, path := range sMux.static { if strings.HasPrefix(req.URL.Path, prefix) { file := path + req.URL.Path[len(prefix):] http.ServeFile(writer, req, file) return } } //动态路由解析 if sMux.m[req.Method] == nil || len(sMux.m[req.Method]) == 0 { http.NotFound(writer, req) return } path := req.URL.Path for _, handlerStruc := range sMux.m[req.Method] { if !handlerStruc.regex.MatchString(path) { continue } matches := handlerStruc.regex.FindStringSubmatch(path) if len(matches[0]) != len(path) { continue } if len(handlerStruc.params) > 0 { //组装请求参数 values := req.URL.Query() for i, val := range matches[1:] { values.Add(handlerStruc.params[i], val) } req.URL.RawQuery = url.Values(values).Encode() } handlerStruc.handler(writer, req) //调用路由相应函数 return } http.NotFound(writer, req) } |
这样,一个满足RESTful规则,而且支持正则匹配的路由器就设计且实现完成了,我们可以来测试一下:
1 2 3 4 5 6 7 8 9 | func main() { sMux := mux.New() sMux.AddGet( "/user/:id([0-9]+)/:name([a-z]+)" , user) log.Println( "server running..." ) log.Fatal(http.ListenAndServe( "localhost:4001" , sMux)) } func user(writer http.ResponseWriter, req *http.Request) { fmt.Fprintln(writer, req.URL.Query()) } |
编译,运行,浏览器访问 http://localhost:4001/user/666/shiajun,正确打印请求参数:
大功告成!
借鉴:
1. https://www.cnblogs.com/xxzhuang/p/9022941.html
2.《Go Web编程》
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)