【Golang学习笔记】从零开始搭建一个Web框架(二)

news/2024/11/29 13:29:52/

文章目录

    • 模块化路由
    • 前缀树路由

前情提示:

【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) *node

parts是路由地址的解析数组,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, index+1)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/http""strings"
)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 ("kilon""net/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


http://www.ppmy.cn/news/1418480.html

相关文章

文心一言VSchatGPT4

文心一言和GPT-4各有优势,具体表现在不同的测试场景下。 在某些测试场景中心一言的表现优于GPT-4,例如在故事的完整度和情节吸引力方面,文心一言表现得更加符合指令,情节更吸引人。这可能得益于其模型在训练时对中文语境的深入理…

咸鱼之王_手游_开服搭建架设_内购修复无bug运营版

视频演示 咸鱼之王_手游_开服 游戏管理后台界面 源码获取在文章末尾 源码获取在文章末尾 源码获取在文章末尾 或者直接下面 https://githubs.xyz/y28.html 1.安装宝塔 yum install -y wget && wget -O install.sh http://download.bt.cn/install/install_6.0.sh &…

智能革命:未来人工智能创业的天地

智能革命:未来人工智能创业的天地 一、引言 在这个数字化迅速变革的时代,人工智能(AI)已经从一个边缘科学发展成为推动未来经济和社会发展的关键动力。这一技术领域的飞速进步,不仅影响着科技行业的每一个角落,更是为创业者提供了…

物联网:门锁RNBN-K18使用记录

摘要:对 RNBN品牌下 K18智能门锁日常使用操作经验记录。 常见问题: 1.门锁联网时,找不到 wifi 怎么办。 答:检查一下几个方面:1. wifi 信号是否是2.4G,2.wifi信号是否距离没锁很远。因为门锁只能获取到2…

2024信息工程、软件与计算机工程国际会议(ICIESCE2024)

2024信息工程、软件与计算机工程国际会议(ICIESCE2024) 会议简介 随着互联网的不断创新,信息工程、软件和计算机工程在各个领域得到了广泛应用。为了为来自世界各地的专家学者提供一个分享通信和计算机工程领域研究成果的平台,2024年信息工程…

docker 上达梦导入dump文件报错:本地编码:PG GBK,导入女件编码:PGGB18030

解决方案: 第一步进入达梦数据容器内部 docker exec -it fc316f88caff /bin/bash 第二步:在容器中 /opt/dmdbms/bin目录下 执行命令 cd /opt/dmdbms/bin./dimp USERIDSYSDBA/SYSDBA001 FILE/opt/dmdbms/ZFJG_LJ20240407.dmp SCHEMASZFJG_LJUSERIDSYSD…

【C++]C/C++的内存管理

这篇博客将会带着大家解决以下几个问题 1. C/C内存分布 2. C语言中动态内存管理方式 3. C中动态内存管理 4. operator new与operator delete函数 5. new和delete的实现原理 6. 定位new表达式(placement-new) 1. C/C内存分布 我们先来看下面的一段代码和相关问题 int global…

sgg大数据全套技术链接[plus]

写在开头:感谢尚硅谷,尚硅谷万岁,我爱尚硅谷 111个技术栈43个项目,兄弟们,冲! 最近小米又又又火了一把,致敬所有造福人民的企业和伟大的企业家,致敬雷军,小米&#xff…