一家专门做原型的网站,mysql创建WordPress,科技网站设计公司有哪些,最有效的线下推广方式文章目录 模块化路由前缀树路由 前情提示#xff1a; 【Golang学习笔记】从零开始搭建一个Web框架#xff08;一#xff09;-CSDN博客
模块化路由
路由在kilon.go文件中导致路由和引擎交织在一起#xff0c;如果要实现路由功能的拓展增强#xff0c;那将会非常麻烦… 文章目录 模块化路由前缀树路由 前情提示 【Golang学习笔记】从零开始搭建一个Web框架一-CSDN博客
模块化路由
路由在kilon.go文件中导致路由和引擎交织在一起如果要实现路由功能的拓展增强那将会非常麻烦这无疑降低了代码的可读性和可维护性。现在的工作是将路由从引擎里剥离出来引擎中仅对路由进行包装。
新建文件router.go当前目录结构为
myframe/├── kilon/│ ├── context.go│ ├── go.mod [1]│ ├── kilon.go│ ├── router.go├── go.mod [2]├── main.go在router中添加下面内容
package kilonimport (net/http
)type router struct {Handlers map[string]HandlerFunc
}
// 创建router对象
func newRouter() *router {return router{make(map[string]HandlerFunc)}
}
// 剥离路由注册的具体实现
func (r *router) addRoute(method string, pattern string, handler HandlerFunc) {key : method - patternr.Handlers[key] handler
}
// 剥离SeverHTTP中路由处理的具体实现
func (r *router) handle(ctx *Context) {key : ctx.Method - ctx.Pathif handler, ok : r.Handlers[key]; ok {handler(ctx)} else {ctx.String(http.StatusNotFound, 404 NOT FOUND: %s\n, ctx.Path)}
}修改kilon.go文件
package kilonimport (net/http
)type HandlerFunc func(*Context)type Origin struct {router *router // 修改路由
}func New() *Origin {return Origin{router: newRouter()} // 修改构造函数
}func (origin *Origin) addRoute(method string, pattern string, handler HandlerFunc) {origin.router.addRoute(method, pattern, handler) // 修改调用
}func (origin *Origin) GET(pattern string, hander HandlerFunc) {origin.addRoute(GET, pattern, hander)
}func (origin *Origin) POST(pattern string, hander HandlerFunc) {origin.addRoute(POST, pattern, hander)
}func (origin *Origin) ServeHTTP(w http.ResponseWriter, req *http.Request) {ctx : newContext(w, req)origin.router.handle(ctx) // 调用router.go中的处理方法
}func (origin *Origin) Run(addr string) (err error) {return http.ListenAndServe(addr, origin)
}至此实现了路由的模块化后续路由功能的增强将不会改动kilon.go文件。
前缀树路由
目前的路由表使用map存储键值对索引非常高效但是有一个弊端键值对的存储的方式只能用来索引静态路由而无法实现动态路由。在实际的应用中可能需要使用正则表达式或者其他匹配规则来实现更复杂的路由匹配而 map 无法提供这种功能。接下来将使用前缀树Tire树实现动态路由主要实现两个功能
参数匹配:。例如 /p/:name/doc可以匹配 /p/zhangsan/doc 和 /p/lisi/doc。通配*仅允许最后一个有*号。例如 /static/*filepath可以匹配/static/fav.ico和/static/js/jQuery.js。
新建文件trie.go当前文件目录结构为
myframe/├── kilon/│ ├── context.go│ ├── go.mod [1]│ ├── kilon.go│ ├── router.go│ ├── tire.go├── go.mod [2]├── main.go在trie.go中创建前缀树的节点:
type node struct {patten string // 待匹配路由part string // 路由当前部分children []*node // 孩子节点isWild bool // 是否为模糊搜索当含有:和通配符*时为true
}当注册路由/p/:name/doc、“/p/:name/png”、“/p/:lang/doc”、/p/:lang/png后树中内容如下 可以看到pattern只有在插入最后一个子节点后才会设置这是为了在查询路由信息时可以根据 pattern来判断改路由是否注册。isWaild的作用在于当part不匹配时如果isWaild为true可以继续搜索这样就实现了模糊匹配。
先实现路由注册时的前缀树插入逻辑
func (n *node) insert(pattern string, parts[]string, index int)pattern是注册路由地址parts是解析pattern后的字符串数组使用方法strings.Split(pattern, /)进行解析如/p/:name/doc对应 [“p”,“:name”,“doc”]parts[index]是当前需要插入的part。可以通过index判断是否退出。疑问如果只用Split解析那pattren/的时候不就无法注册了吗答开始时树的根节点的part为空不会匹配“p一定会插入到根节点的子节点切片中。而当pattern为”/“时解析字符串切片为空进入根节点的时候len(parts) index 0,会将根节点的pattern设置为”/“也可以实现路由”/的注册。
代码如下
func (n *node) insert(pattern string, parts[]string, index int){// 进来的时候说明 n.part parts[index-1] 即最后一个 part 则直接设置 pattenif len(parts) index {n.patten patternreturn}// 还需匹配 part// 先在 n.children 切片中匹配 partpart : parts[index]child : n.matchChild(part)// 如果没有找到则构建一个 child 并插入 n.children 切片中if child nil {child node{part: part,// 含有:或者通配符*时为 trueisWild: part[0] : || part[0] *,}// 插入 n.children 切片n.children append(n.children, child)}// 递归插入child.insert(pattern, parts, index 1)
}
// 查找匹配 child
func (n *node) matchChild(part string) *node {// 遍历 n.children 查找 part 相同的 childfor _, child : range n.children {// 如果找到匹配返回 child 当 isWild 为 true 时视为匹配实现模糊搜索if child.part part || child.isWild true {return child}} // 没找到返回nilreturn nil
}接下来实现接受请求时查询路由信息时的前缀树搜索逻辑
func (n *node) search(parts []string, index int) *nodeparts是路由地址的解析数组index指向当前part索引
代码如下:
// 搜索
func (n *node) search(parts []string, index int) *node {// 如果匹配将节点返回if len(parts) index || strings.HasPrefix(n.part, *) {if n.pattern {return nil}return n}part : parts[index]// 获取匹配的所有孩子节点nodes : n.matchChildren(part)// 递归搜索匹配的child节点for _, child : range nodes {result : child.search(parts, index1)if result ! nil {return result}}return nil
}
// 查找匹配的孩子节点由于有:和*所以可能会有多个匹配因此返回一个节点切片
func (n *node) matchChildren(part string) []*node {nodes : make([]*node, 0)for _, child : range n.children {if child.part part || child.isWild true {nodes append(nodes, child) // 将符合的孩子节点添入返回切片}}return nodes
}至此trie.go暂时写完现在在路由中进行应用回到router.go文件。为了区分不同的方法如GET和POST为每一个Method建立一颗前缀树并以键值对的形式存储在一个map中map[Method] tire。修改router结构体与构造方法
type router struct {roots map[string]*node // 前缀树mapHandlers map[string]HandlerFunc // 将pattern作为key获取/注册方法
}
func newRouter() *router {return router{make(map[string]*node),make(map[string]HandlerFunc),}
}将pattern插入前缀树之前要先解析成字符串切片现在需要实现一个解析函数。
func parsePattern(pattern string) []string {temp : strings.Split(pattern, /)parts : make([]string, 0)for _, item : range temp {if item ! {parts append(parts, item)if item[0] * {break}} }return parts
}修改注册路由的逻辑
func (r *router) addRoute(method string, pattern string, handler HandlerFunc) {parts : parsePattern(pattern) // 解析patternkey : method - patternif _, ok : r.roots[key]; !ok {r.roots[method] node{} // 如果没有则创建一个节点}r.roots[method].insert(pattern, parts, 0) // 前缀树插入patternr.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)if n! nil {parts : parsePattern(n.pattern) // 解析pattern// 寻找*和:,找到对应的参数。for index, part : range parts {if part[0] : {params[part[1:]] searchParts[index]}if part[0] * len(part) 1 {// 将*后切片内容拼接成路径params[part[1:]] strings.Join(searchParts[index:],/)break // 仅允许一个通配符*}return n, params}}return nil, nil
}路径中的参数应该交给上下文对象让用户便捷获取。在Context结构体中添加Params属性,并包装获取方法
type Context struct {Writer http.ResponseWriterReq *http.RequestPath stringMethod stringParams map[string]string // 路由参数属性StatusCode int
}
// 获取路径参数
func (c *Context) Param(key string) string {value : c.Params[key]return value
}在router.go中的handle中应用路由获取方法并将路径参数提交给上下文对象。
func (r *router) handle(ctx *Context) {n, params : r.getRoute(ctx.Method, ctx.Path) // 获取路由节点及参数字典ctx.Params paramsif n ! nil {key : ctx.Method - n.pattern // key为n的patternr.Handlers[key](ctx) // 调用注册函数} else {ctx.String(http.StatusNotFound, 404 NOT FOUND: %s\n, ctx.Path)}
}现在router.go内容为
package kilonimport (net/httpstrings
)type router struct {roots map[string]*nodeHandlers map[string]HandlerFunc
}func newRouter() *router {return router{make(map[string]*node),make(map[string]HandlerFunc),}
}func (r *router) addRoute(method string, pattern string, handler HandlerFunc) {parts : parsePattern(pattern)key : method - pattern_, ok : r.roots[method]if !ok {r.roots[method] node{}}r.roots[method].insert(pattern, parts, 0)r.Handlers[key] handler
}func (r *router) handle(ctx *Context) {n, params : r.getRoute(ctx.Method, ctx.Path)ctx.Params paramsif n ! nil {key : ctx.Method - n.patternr.Handlers[key](ctx)} else {ctx.String(http.StatusNotFound, 404 NOT FOUND: %s\n, ctx.Path)}
}func parsePattern(pattern string) []string {temp : strings.Split(pattern, /)parts : make([]string, 0)for _, item : range temp {if item ! {parts append(parts, item)if item[0] * {break}}}return parts
}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)if n ! nil {parts : parsePattern(n.pattern)for index, part : range parts {if part[0] : {params[part[1:]] searchParts[index]}if part[0] * len(part) 1 {params[part[1:]] strings.Join(searchParts[index:], /)break}}return n, params}return nil, nil
}在main.go测试一下
package mainimport (kilonnet/http
)func main() {r : kilon.New()r.GET(/hello, func(ctx *kilon.Context) {ctx.JSON(http.StatusOK, kilon.H{message: Hello World,})})r.GET(/hello/:username, func(ctx *kilon.Context) {ctx.JSON(http.StatusOK, kilon.H{message: ctx.Param(username),})})r.GET(/hello/:username/*filename, func(ctx *kilon.Context) {ctx.JSON(http.StatusOK, kilon.H{username: ctx.Param(username),filename: ctx.Param(filename),})})r.Run(:8080)
}分别访问下面地址都可以看到响应信息
127.0.0.1:8080/hello
127.0.0.1:8080/hello/zhangsan
127.0.0.1:8080/hello/zhangsan/photo.png