本文主要讲解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 是一个接口类型,源码如下:

type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}

所以我们自定义的路由器也需要实现这个接口。事实上go默认的路由器类型 ServeMux 也实现了这个接口,http服务处理客户端请求时会调用路由器的方法 ServeHTTP(ResponseWriter, *Request) 进行路由匹配和请求分发。这是http服务器和路由器唯一有交集的地方,所以只要我们自定义的路由器也定义方法 ServeHTTP(ResponseWriter, *Request) 并在其中实现我们自定义的路由匹配和请求分发,就完全可以使用我们自定义的路由器取代go默认的路由器,至于路由器结构怎么设计,路由怎么注册,又怎么匹配,都是我们说了算。

接下来先写一个简单的自定义路由器,取代go默认路由器,代码如下:

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 的值则是路由结构体的切片(路由结构暂时未知)。这样做的好处是,进行路由匹配的时候,可以先根据请求方法过滤掉其他请求方法的路由,只需遍历当前请求方法的路由结构体切片进行匹配即可。动态路由使用正则匹配,而静态路由比较简单,使用请求文件路径前缀匹配即可。考虑到并发请求,再加一个读写锁,所以路由器结构如下:

//路由器结构
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 ,所以路由结构体的结构如下:

//路由结构
type HandlerStruc struct {
regex *regexp.Regexp //正则对象
params map[int]string //请求参数
handler Handler //响应函数
}
type Handler func(http.ResponseWriter, *http.Request)

创建路由器对象的方法:

//创建路由器
func New() *simMux {
return &simMux{
sync.RWMutex{},
make(map[string][]HandlerStruc),
make(map[string]string),
}
}

接下来就要实现一个注册路由的方法,注册路由的过程其实上面已经都解释得很清楚了,直接上代码:

//注册静态路由
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
}

然后封装各个请求方法的路由注册方法,方便调用:

//注册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)
}

最后就是路由匹配与请求分发的实现了。根据请求方法定位到具体路由结构体切片,遍历切片进行正则匹配,若匹配成功,拼接请求参数,调用对应响应函数,代码如下:

//路由匹配与请求分发
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规则,而且支持正则匹配的路由器就设计且实现完成了,我们可以来测试一下:

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编程》

go web编程——自定义路由设计的更多相关文章

  1. web项目自定义路由_实现静态资源URL控制

    前言: IIS会默认把:图片.JS.HTML.CSS这些文件当成静态资源处理,为了减少服务器压力,默认这些静态资源是不走URL路由规则控制的. 作为小白及初学者,本人对这些了解甚少,补充基础知识吧: ...

  2. go web编程——路由与http服务

    本文主要讲解go语言web编程中的路由与http服务基本原理. 首先,使用go语言启动一个最简单的http服务: package main import ( "log" " ...

  3. golang web框架设计2:自定义路由

    继续学习谢大的Go web框架设计 HTTP路由 http路由负责将一个http的请求交到对应的函数处理(或者一个struct的方法),路由在框架中相当于一个事件处理器,而这个时间包括 用户请求的路径 ...

  4. Web API配置自定义路由

    默认访问Web API时,是无需指定method名.它会按照默认的路由来访问.如果你的Web API中出现有方法重载时,也许得配置自定义路由: 标记1为自定义路由,标记2为默认路由,需要把自定义路由排 ...

  5. go web编程——session管理机制设计与实现

    原生Go语言没有实现session管理机制,所以如果使用原生Go语言进行web编程,我们需要自己进行session管理机制的设计与实现,本文将就此进行详细介绍,并实现一个简单的session管理机制. ...

  6. 物联网网络编程、Web编程综述

    本文是基于嵌入式物联网研发工程师的视觉对网络编程和web编程进行阐述.对于专注J2EE后端服务开发的童鞋们来说,这篇文章可能稍显简单.但是网络编程和web编程对于绝大部分嵌入式物联网工程师来说是一块真 ...

  7. Python 四大主流 Web 编程框架

    Python 四大主流 Web 编程框架 目前Python的网络编程框架已经多达几十个,逐个学习它们显然不现实.但这些框架在系统架构和运行环境中有很多共通之处,本文带领读者学习基于Python网络框架 ...

  8. PHP求职宝典系列——PHP Web 编程篇

    PHP Web 编程篇 form表单 1.简述 POST 和 GET 传输的最大容量分别是多少? GET 方法提交的表单数据被附加到 URL 上,并作为URL 的一部分发送到服务器端. URL 的长度 ...

  9. MVC系列——MVC源码学习:打造自己的MVC框架(三:自定义路由规则)

    前言:上篇介绍了下自己的MVC框架前两个版本,经过两天的整理,版本三基本已经完成,今天还是发出来供大家参考和学习.虽然微软的Routing功能已经非常强大,完全没有必要再“重复造轮子”了,但博主还是觉 ...

随机推荐

  1. LinuxC语言实现服务端与客户端多进程通信

    链接:https://pan.baidu.com/s/1YDNIyTKAkh4E5x2dBeTgcQ 提取码:y35q 复制这段内容后打开百度网盘手机App,操作更方便哦 本实验用的是Centos7m ...

  2. [python 学习]正则表达式

    re 模块函数re 模块函数和正则表达式对象的方法match(pattern,string,flags=0) 尝试使用带有可选的标记的正则表达式的模式来匹配字符串.如果匹配成功,就返回匹配对象:如果失 ...

  3. Test 6.29 T2 染色

    问题描述 于是 CJK 轻轻松松就切了第一题."好,那么来看看第二题吧." JesseLiu 大手一挥,CJK 眼前立刻出现了一棵有 n 个节点的树."现在,你将要为这颗 ...

  4. OC+RAC(六) 核心方法bind

    -(void)_test6{ RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> ...

  5. Android逆向之旅---解析编译之后的Resource.arsc文件格式

    一.前言 快过年了,先提前祝贺大家新年快乐,这篇文章也是今年最后一篇了.今天我们继续来看逆向的相关知识,前篇文章中我们介绍了如何解析Android中编译之后的AndroidManifest.xml文件 ...

  6. Cluster基础(四):创建RHCS集群环境、创建高可用Apache服务

    一.创建RHCS集群环境 目标: 准备四台KVM虚拟机,其三台作为集群节点,一台安装luci并配置iSCSI存储服务,实现如下功能: 使用RHCS创建一个名为tarena的集群 集群中所有节点均需要挂 ...

  7. zhanghao

    账号:wx8b9ddd1c943ce95f 密码:fa72de9a1721849edc7f41f8a81019e5

  8. vue-lazyload 图片不更新

    前几天在用vue写项目的时候,因为图片比较多,所以采用了懒加载插件 vue-lazyload github:https://github.com/hilongjw/vue-lazyload#readm ...

  9. Git学习及使用

    一.认知git理论 1.git出现的背景 版本控制 版本控制是一种记录一个或若干文件内容变化,以便将来查阅特定版本修订情况的系统. 许多人习惯用复制整个项目目录的方式来保存不同的版本,或许还会改名加上 ...

  10. sticky用法

    可以用于滚动到一定距离以后让他实现定位效果. 比如滚动到50px的时候让导航栏固定定位. 用法:给最外层的div设置绝对定位 然后要固定的div设置position:sticky; top:0: 这样 ...