如果我们想支持类似于 /hello/:name
Trie 树简介
先来介绍一下动态路由,感觉类似于分组路由
所谓动态路由,就是一条路由规则可以匹配某一类型而非固定的路由,例如:hello/:name
,可以匹配 hello/geektutu
。 hello/jack
前缀树就是所有的子节点都有一个共同的祖先节点
识记内容:
-
参数匹配
:
eg:/p/:lang/doc
->/p/c/doc
or/p/xzc/doc
-
通配
*
static/*filepath
->/static/fav.ico
这样的一般用于静态服务器,能够递归的匹配子路径
Trie树实现
package gee
import "strings"
type node struct {
pattern string // 待匹配路由,例如 /p/:lang
part string //路由中的一部分 :lang
children []*node //子节点,例如[doc,intro]
isWild bool //是否精确匹配 part含有:或 *时为true
/*
isWild成功保障动态路由的实现
*/
}
// 第一个匹配成功的节点,用于插入
// 这是用来在trie树上插入新的节点位置的函数
// 匹配单个节点在整个字符串的位置
func (n *node) matchChild(part string) *node {
for _, child := range n.children {
if child.part == part || child.isWild {
return child
}
}
return nil
}
// 所有匹配成功的节点,用于查找
// 客户端输入的URL匹配响应的路由规则
func (n *node) matchChildren(part string) []*node {
nodes := make([]*node, 0)
for _, child := range n.children {
if child.part == part || child.isWild {
nodes = append(nodes, child)
}
}
return nodes
}
func (n *node) insert(pattern string, parts []string, height int) {
if len(parts) == height { //height是用来判断递归的深度
n.pattern = pattern //如果匹配到结尾还没有匹配成功的话,就直接加到后面
return
}
part := parts[height]
child := n.matchChild(part)
if child == nil {
//这说明也没有匹配成功,然后需要准备添加一个节点了
//添加节点主要是判断最后面是不是通配符也就是判断isWild的值
child = &node{part: part, isWild: part[0] == ':' || part[0] == '*'}
n.children = append(n.children, child)
}
child.insert(pattern, parts, height+1)
}
func (n *node) search(parts []string, height int) *node {
if len(parts) == height || strings.HasPrefix(n.part, "*") {
/*
如果当前搜索的URL到达末尾或者说当前的节点是通配符
*/
if n.pattern == "" {
//如果如果我当前匹配的字符串为空的话
return nil
}
return n
}
part := parts[height]
//获取当前路径部分
children := n.matchChildren(part)
for _, child := range children {
result := child.search(parts, height+1)
if result != nil {
return result //返回对应找到了的节点
}
}
return nil //如果一直没有找到
}
/*
在一个典型的路由系统中,会有很多的路由规则,
每个规则对应着一个 URL 模式以及对应的处理函数。当客户端发送一个请求时,
路由系统会根据请求的 URL,逐个匹配所有的路由规则,
直到找到与请求路径完全匹配的规则为止。一旦找到了匹配的路由规则,路由系统会立即返回对应的处理函数,以处理该请求。
这种按需匹配的方式非常高效,因为它避免了不必要的匹配过程,只在需要时才匹配规则。同时,它也保证了每个请求都会被正确地路由到对应的处理函数,确保了系统的正常运行。
*/
router
package gee
import (
"net/http"
"strings"
)
type router struct {
handlers map[string]HandlerFunc
roots map[string]*node
}
func newRouter() *router {
return &router{
handlers: make(map[string]HandlerFunc),
roots: make(map[string]*node)}
}
// 将路径当中的/和空去掉
func parsePattern(pattern string) []string {
vs := strings.Split(pattern, "/") //分割成路径部分
//诶个元素代表路径模式中的一个部分
parts := make([]string, 0)
for _, item := range vs {
if item != "" { //排除空路径部分
parts = append(parts, item)
if item[0] == '*' { //通配符后面的部分不在进行匹配,可以直接忽略
break
}
}
}
return parts
}
func (r *router) addRouter(method string, pattern string, handler HandlerFunc) {
parts := parsePattern(pattern)
key := method + "-" + pattern
//查看是否有这种方法
_, ok := r.roots[method] //如果没有这种HTTP方法
//需要创建一个树的根节点,在这个树下面全部和这个方法有关的路由规则
if !ok {
r.roots[method] = &node{} //创建
}
r.roots[method].insert(pattern, parts, 0)
r.handlers[key] = handler
}
func (r *router) getRoute(method string, path string) (*node, map[string]string) {
//找到某个方法下面的路径
searchParts := parsePattern(path) //将这个路径节点化
params := make(map[string]string) //用于存储匹配到的参数
root, ok := r.roots[method]
//首先找这个方法是否存在
if !ok {
return nil, nil
}
n := root.search(searchParts, 0)
//开始在此方法下面的路由器中搜索
//n是对应的节点
if n != nil {
parts := parsePattern(n.pattern)
for index, part := range parts {
if part[0] == ':' {
//如果是动态
params[part[1:]] = searchParts[index] //searchParts是原来的节点
//将参数名和对应的值存储到对应的params
}
if part[0] == '*' && len(part) > 1 {
params[part[1:]] = strings.Join(searchParts[index:], "/")
//
break
}
}
return n, params
//返回对应的节点和与请求路径匹配的路由规则
}
return nil, nil
}
/*
也就是说我返回的是一个节点和
我匹配成功的那个节点的路由规则和
我传过来路径匹配成功时刻节点
所构成的键值对
*/
func (r *router) handle(c *Context) {
key := c.Method + "-" + c.Path
if handler, ok := r.handlers[key]; ok {
handler(c) //后面的c应该改变了
} else {
c.String(http.StatusNotFound, "404 NOT FOUND: %s\n", c.Path)
}
}
对代码进行人工调试:
我们发现就第一条语句 gee.New()
:而言
func New() *Engine {
return &Engine{router: newRouter()}
}
func newRouter() *router {
return &router{
handlers: make(map[string]HandlerFunc),
roots: make(map[string]*node)}
//现在我们路由包括了一个处理程序还有一个用于查找/储存URL的节点
}
type HandlerFunc func(*Context)
type Context struct {
//origin object 起始对象
Writer http.ResponseWriter
Req *http.Request
//request info
Path string
Method string
Params map[string]string
//respnse info
StatusCode int
}
type node struct {
pattern string // 待匹配路由,例如 /p/:lang
part string //路由中的一部分 :lang
children []*node //子节点,例如[doc,intro]
isWild bool //是否精确匹配 part含有:或 *时为true
/*
isWild成功保障动态路由的实现
*/
}
我们现在Engine还是主要包含路由器router,router里面现在又包含了一个处理程序和一个Tree的结构
r.GET("/", func(c *gee.Context) {
c.HTML(http.StatusOK, "<h1>Hello Gee</h1>")
})
func (engine *Engine) GET(pattern string, handler HandlerFunc) {
engine.addRoute("GET", pattern, handler)
}
func (engine *Engine) addRoute(method string, pattern string, handler HandlerFunc) {
engine.router.addRoute(method, pattern, handler)
}
func (r *router) addRoute(method string, pattern string, handler HandlerFunc) {
parts := parsePattern(pattern)
key := method + "-" + pattern
//查看是否有这种方法
_, ok := r.roots[method] //如果没有这种HTTP方法
//需要创建一个树的根节点,在这个树下面全部和这个方法有关的路由规则
if !ok {
r.roots[method] = &node{} //创建一个根节点
}
r.roots[method].insert(pattern, parts, 0)
//对于新路径的增加
r.handlers[key] = handler
}
func (n *node) insert(pattern string, parts []string, height int) {
//这个函数的主要需求是插入一个新的node
if len(parts) == height { //height是用来判断递归的深度
n.pattern = pattern //如果匹配到结尾还没有匹配成功的话,就直接加到后面
return
}
part := parts[height]
child := n.matchChild(part)
if child == nil {
//找到分支点了
//准备纪念性插入的工作
//这个代码昨天还不是很懂,今天就感觉懂了很多了
child = &node{part: part, isWild: part[0] == ':' || part[0] == '*'}
n.children = append(n.children, child)
}
child.insert(pattern, parts, height+1)
//注意:我们循环的入口是child调用的,这意味着,我们每次进行matchChild是遍历循环当前节点的所有叶子节点,如果找到了,就会继续往下面推进
}
func (n *node) matchChild(part string) *node {
for _, child := range n.children {
if child.part == part || child.isWild {
return child
}
}
return nil
}
type node struct {
pattern string // 待匹配路由,例如 /p/:lang
part string //路由中的一部分 :lang
children []*node //子节点,一般来说子节点其实就是一条路径
isWild bool //是否精确匹配 part含有:或 *时为true
/*
isWild成功保障动态路由的实现
*/
}
r.GET("/hello", func(c *gee.Context) {
// expect /hello?name=geektutu
c.String(http.StatusOK, "hello %s, you're at %s\n", c.Query("name"), c.Path)
/*
这里相当于回应一个响应消息,我想知道怎么回应的
通过这个方法将Context准备完成
->handler准备完成
->writer准备完成,其实我感觉handler是辅助writer的
现在开始复习第一节的内容,我们设置一个函数其实是为了反馈到
*/
}
/*
第一个参数会保存到方法GET
*/)
func (c *Context) Query(key string) string {
return c.Req.URL.Query().Get(key) //获取指定名称的查询参数的值
/*
这里的Query是url.Values类型的方法
用于获取指定键的第一个值
http://example.com/?name=John&age=30&name=Jane
这个查询参数包含了两个键值对:name=John 和 age=30,其中键 name 对应了两个值:John 和 Jane。
如果我们使用 url.Values 类型来表示这个查询参数,
并调用 Get 方法来获取 name 键对应的值,
那么 Get 方法会返回第一个值,也就是 John。
*/
}
func (c *Context) String(code int, format string, values ...interface{}) {
c.SetHeader("Content-Type", "text/plain")
c.Status(code) //这里设置状态码
c.Writer.Write([]byte(fmt.Sprintf(format, values...)))
/*
format是一个字符串参数,用于指定返回的纯文本格式
value...interface{} 这个是用来传递多个值,用于填充format字符串中的字符串
*/
}
func (r *router) addRoute(method string, pattern string, handler HandlerFunc) {
parts := parsePattern(pattern)
key := method + "-" + pattern
//查看是否有这种方法
_, ok := r.roots[method] //如果没有这种HTTP方法
//需要创建一个树的根节点,在这个树下面全部和这个方法有关的路由规则
if !ok {
r.roots[method] = &node{} //创建
}
r.roots[method].insert(pattern, parts, 0) //我们现在主要是研究匿名函数的设置
r.handlers[key] = handler//可以发现我们将匿名函数设置在了一个map当中
}
type router struct {
handlers map[string]HandlerFunc
roots map[string]*node
}
type HandlerFunc func(*Context)
type Context struct {
//origin object 起始对象
Writer http.ResponseWriter
Req *http.Request
//request info
Path string
Method string
Params map[string]string
//respnse info
StatusCode int
}
这个函数的目的最终是生成Context,通过函数的不断封装,转化,最终会成功生成一个Context
感觉自己消化的并不是很好,开始复习前面的内容
Handler是一个 接口,需要实现方法ServeHTTP,
我们的ListenAndServe方法的第二个参数类型就是Handler,也就是说只要实现了Handler接口,就可以作为第二个参数
所有的HTTP请求,都会交给该实例处理