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

文章目录

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

前情提示:

【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 kilon

import (
	"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 + "-" + pattern
	r.Handlers[key] = handler
}
// 剥离SeverHTTP中路由处理的具体实现
func (r *router) handle(ctx *Context) {
	key := ctx.Method + "-" + ctx.Path
	if handler, ok := r.Handlers[key]; ok {
		handler(ctx)
	} else {
		ctx.String(http.StatusNotFound, "404 NOT FOUND: %s\n", ctx.Path)
	}
}

修改kilon.go文件:

package kilon

import (
	"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 则直接设置 patten
	if len(parts) == index {
		n.patten = pattern
		return
	}
	// 还需匹配 part
	// 先在 n.children 切片中匹配 part
	part := parts[index]
	child :=  n.matchChild(part)
	// 如果没有找到,则构建一个 child 并插入 n.children 切片中
	if child == nil {
		child = &node{
			part: part,
			// 含有":"或者通配符"*"时为 true
			isWild: 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 相同的 child
	for _, child := range n.children {
		// 如果找到匹配返回 child, 当 isWild 为 true 时视为匹配实现模糊搜索
		if child.part == part || child.isWild == true {
			return child
		}
	}	
	// 没找到返回nil
	return 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       // 前缀树map
	Handlers 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) // 解析pattern

	key := method + "-" + pattern

	if _, ok := r.roots[key]; !ok {
		r.roots[method] = &node{} // 如果没有则创建一个节点
	}

	r.roots[method].insert(pattern, parts, 0) // 前缀树插入pattern
	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)

	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.ResponseWriter
	Req        *http.Request
	Path       string
	Method     string
	Params     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 = params
	if n != nil {
		key := ctx.Method + "-" + n.pattern // key为n的pattern
		r.Handlers[key](ctx) // 调用注册函数
	} else {
		ctx.String(http.StatusNotFound, "404 NOT FOUND: %s\n", ctx.Path)
	}
}

现在router.go内容为:

package kilon

import (
	"net/http"
	"strings"
)

type router struct {
	roots    map[string]*node
	Handlers 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 = params
	if n != nil {
		key := ctx.Method + "-" + n.pattern
		r.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 main

import (
	"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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/541738.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Python项目移动文件夹后出现ModuleNotFoundError: No module named

文章目录 0 问题再现1 问题原因2 结局办法2.1 如果是使用编译器运行2.2 如果是在linux环境下,使用命令运行 0 问题再现 移动Python的项目文件夹后,代码出现模块引用未知的问题。 1 问题原因 移动Python项目后,source root(项目…

创新者指南

创新者指南 怎么把一个模糊的想法变为成功的商业模式。在高度不确定的商业环境下,怎样验证创新想法是否有商业价值,还有怎么用较低的成本打造产品,并推向市场。作者为我们总结出3个有用的策略(测试方法)和几个关键步骤…

从零实现诗词GPT大模型:专栏内容规划

一、前情介绍 本系列文章将从头编写一个类GPT的深度学习模型,并在诗词数据集上进行训练,从而可以进行诗词创作。 本次实现的类GPT模型,可以在kaggle上使用免费GPU进行训练,并可以在自己的电脑上进行推理,整个学习过程…

前端vue: 使用ElementUI适配国际化

i18n介绍 i18n(其来源是英文单词 internationalization的首末字符i和n,18为中间的字符数)是“国际化”的简称。 前端国际化步骤 1、安装i18n插件 安装插件时候,注意必须指定版本号,不然安装会报错。 npm i vue-i1…

查天气(Vue.js,Element UI)

演示图 几点注意 有亿点简陋,凑合能用,button一定要 !important 覆盖原本的 element ui ,不然无效axios回调函数中 this 指向改变了,需要额外的保存一份服务器返回的数据比较复杂时,获取的时候需要注意层级结构method…

分类预测 | Matlab实现RIME-LSSVM霜冰算法优化最小二乘支持向量机数据分类预测

分类预测 | Matlab实现RIME-LSSVM霜冰算法优化最小二乘支持向量机数据分类预测 目录 分类预测 | Matlab实现RIME-LSSVM霜冰算法优化最小二乘支持向量机数据分类预测分类效果基本介绍程序设计参考资料 分类效果 基本介绍 1.Matlab实现RIME-LSSVM霜冰算法优化最小二乘支持向量机数…

标准更新丨美国发布玩具安全标准ASTM F963-23

2023年10月13日,美国材料试验协会 (ASTM)发布了玩具安全标准ASTM F963-23。新版标准主要修订了声响、电池可触及性、膨胀材料和弹射玩具的技术要求,另外,澄清和调整了邻苯二甲酸酯、玩具基材重金属的豁免以及溯源标签的要求,使其保…

vim相关指令

vim的各种模式及其转换关系图 vim 默认处于命令模式!!! 模式之间转换的指令 除【命令模式】之外,其它模式要切换到【命令模式】,只需要无脑 ESC 即可!!! [ 命令模式 ] 切换至 [ 插…

SQL优化之EXPLAIN执行计划(转载)

目录 第一章、快速了解EXPLAIN1.1)EXPLAIN是什么1.2)示例 第二章、结果列说明2.1)id 与table2.2)select_type:2.3)type2.4)possible_keys与key2.5) key_len2.6)rows2.7&a…

Android 加密之 打包为arr 项目依赖或者为jar

Android 加密之 打包为arr 项目依赖或者为jar 1. 修改build.gradle plugins {//id com.android.application// 1. 修改为libraryid com.android.library }android {namespace com.dzq.iccid2compileSdk 33defaultConfig {//applicationId "com.dzq.iccid2"// 2. 注…

Docker容器基本操作从入门到大牛

1. Docker安装部署 1.1 openEuler使用YUM安装 [rootnode1 ~]# yum install docker -y [rootnode1 ~]# docker version Client:Version: 18.09.0EulerVersion: 18.09.0.332API version: 1.39Go version: go1.17.3Git commit: 9942888Built…

基于PyTorch神经网络进行温度预测——基于jupyter实现

导入环境 import numpy as np import pandas as pd import matplotlib.pyplot as plt import torch import torch.optim as optim import warnings warnings.filterwarnings("ignore") %matplotlib inline读取文件 ### 读取数据文件 features pd.read_csv(temps.…

突破编程_前端_SVG(rect 矩形)

1 rect 元素的基本属性和用法 在SVG中&#xff0c;<rect> 元素用于创建矩形。 <rect> 元素有一些基本的属性&#xff0c;可以用来定义矩形的形状、位置、颜色等。以下是这些属性的详细解释&#xff1a; x 和 y &#xff1a;这两个属性定义矩形左上角的位置。 x …

供应链复杂业务实时数仓建设之路

供应链复杂业务实时数仓建设之路 背景 供应链业务是纷繁复杂的&#xff0c;我们既有 JIT 的现货模式中间夹着这大量的仓库作业环节&#xff0c;又有到仓的寄售&#xff0c;品牌业务&#xff0c;有非常复杂的逆向链路。在这么复杂的业务背后&#xff0c;我们需要精细化关注人货…

JVM复习

冯诺依曼模型与计算机处理数据过程相关联&#xff1a; 冯诺依曼模型&#xff1a; 输入/输出设备存储器输出设备运算器控制器处理过程&#xff1a; 提取阶段&#xff1a;输入设备传入原始数据&#xff0c;存储到存储器解码阶段&#xff1a;由CPU的指令集架构ISA将数值解…

6.4Python之字典的可变数据类型

字典是不可变数据类型&#xff0c;但其值都是可变数据类型。添加修改删除&#xff0c;都是通过改变寻址的方式做数据的变化。 例如&#xff1a; d1 {"a": 1, "b": 2, "c": 5} print(d1) print(id(d1)) print(d1["c"], "的ID&a…

策略者模式(代码实践C++/Java/Python)————设计模式学习笔记

文章目录 1 设计目标2 Java2.1 涉及知识点2.2 实现2.2.1 实现两个接口飞行为和叫行为2.2.2 实现Duck抽象基类&#xff08;把行为接口作为类成员&#xff09;2.2.3 实现接口飞行为和叫行为的具体行为2.2.4 具体实现鸭子2.2.5 模型调用 3 C&#xff08;用到了大量C2.0的知识&…

笔记本台式机电脑 “睡眠和休眠”有什么区别,那个更省电

笔记本台式机电脑 Windows 系统里睡眠和休眠有什么区别&#xff0c;睡眠和休眠那个更省电&#xff0c;睡眠和休眠使用那个更好&#xff0c;当不用电脑时&#xff0c;通常有三种方式让电脑休息&#xff1a;关机、睡眠和休眠。关机的定义大家都懂&#xff0c;但睡眠和休眠就容易让…

IEDA 启动项目时出现 java: java.lang.OutOfMemoryError: GC overhead limit exceeded 异常

问题 通过Idea启动项目时&#xff0c;出现java: java.lang.OutOfMemoryError: GC overhead limit exceeded 内存溢出问题&#xff1b; 解决方案 错误是发生在编译阶段&#xff0c;而不是运行阶段&#xff1a; 【1】idea编译Java项目使用的虚拟机和idea软件自身使用的虚拟机是…

APP开发教学:开发同城O2O外卖跑腿系统源码详解

同城O2O外卖跑腿系统&#xff0c;满足了人们对于外卖送餐和生活服务的需求。今天&#xff0c;小编将为您讲解如何开发同城O2O外卖跑腿系统源码。 1.前期准备 首先&#xff0c;我们需要明确系统的功能需求和用户需求&#xff0c;包括外卖订购、配送员接单、支付功能等。其次&am…