【GO基础学习】gin框架路由详解
文章目录
- gin框架路由详解
- (1)go mod tidy
- (2)r := gin.Default()
- (3)r.GET()
- 路由注册
- (4)r.Run()
- 路由匹配
- 总结
gin框架路由详解
先创建一个项目,编写一个简单的demo,对这个demo进行讲解。
- 创建一个go的项目,采用GoLand:
2. 在该项目下创建一个main.go
文件:
package mainimport ("net/http""github.com/gin-gonic/gin"
)func main() {r := gin.Default()r.GET("/", func(c *gin.Context) {c.String(http.StatusOK, "hello word")})r.Run(":8000")
}
上面就是一个非常简单的gin的使用,逐行代码解读,在解读前,需要先下载gin库,写入main.go
后,在terminal执行go mod tidy
命令就会添加main里面的gin库。
(1)go mod tidy
- 添加缺失的依赖
如果你的代码中引用了某些依赖(通过 import
),但它们没有被记录在 go.mod
文件中,go mod tidy
会自动将这些缺失的依赖添加到 go.mod
文件中。
- 移除未使用的依赖
如果你的代码中不再使用某些依赖(即没有通过 import
引用),go mod tidy
会从 go.mod
文件中移除这些无用的依赖。
- 更新
go.sum
文件
go mod tidy
会检查 go.sum
文件(存储模块的校验和)是否与 go.mod
文件一致:
-如果某些依赖的校验和缺失,它会添加。
-如果某些校验和多余(对应的依赖已被移除),它会删除。
(2)r := gin.Default()
创建默认的Gin引擎,点进这个方法查看源码:
// Default returns an Engine instance with the Logger and Recovery middleware already attached.
func Default(opts ...OptionFunc) *Engine {debugPrintWARNINGDefault()engine := New()engine.Use(Logger(), Recovery())return engine.With(opts...)
}
返回了Engine的指针结构体,还包括一些日志和中断复原的操作。
关于Engine结构体:【是 Gin 框架的核心结构体,它既是路由表的管理器,也是 HTTP 服务的入口】
type Engine struct {RouterGrouptrees methodTrees // 每种 HTTP 方法对应的路由树maxParams uint16 // 路由参数最大数量maxSections uint16 // 路由路径最大分段数量handlers404 HandlersChain // 404 处理函数链// 其他字段...
}
-
trees
: 存储路由表的核心字段,每种 HTTP 方法有一棵对应的 Radix 树。 -
RouterGroup
: 用于管理路由组和中间件。 -
handlers404
: 默认的 404 错误处理。
关于trees
是路由规则的核心,存储路由表,每种 HTTP 方法有一棵对应的 Radix 树。
(1)Radix 树
公共前缀的树结构,是一种更节省空间的前缀树(Trie Tree)。对于基数树的每个节点,如果该节点是唯一的子树的话,就和父节点合并。下图为一个基数树示例:
(2)methodTrees
type methodTree struct {method stringroot *node
}type methodTrees []methodTree
method
是http的类型,每个路由路径的片段都由一个node
节点构成:
type node struct {path string // 当前节点的路径部分indices string // 子节点的索引,用于快速查找children []*node // 子节点handlers HandlersChain // 当前节点的处理函数priority uint32 // 优先级,用于优化匹配顺序wildChild bool // 是否包含通配符子节点nType nodeType // 节点类型: static, param, catchAll
}
path
: 存储路径片段。
indices
: 子节点索引,表示每个子节点的第一个字符,用于快速查找。
handlers
: 当前节点绑定的处理函数。
wildChild
: 是否有动态或通配符子节点。
nType
:
static
: 静态路径节点。param
: 动态路径节点(如:id
)。catchAll
: 通配符节点(如*filepath
)。
(3)r.GET()
Gin 的路由实现主要分为路由注册和路由匹配两部分。
路由注册
点进GET方法:
// GET is a shortcut for router.Handle("GET", path, handlers).
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {return group.handle(http.MethodGet, relativePath, handlers)
}
handle
方法:
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {absolutePath := group.calculateAbsolutePath(relativePath)handlers = group.combineHandlers(handlers)group.engine.addRoute(httpMethod, absolutePath, handlers)return group.returnObj()
}
addRoute
方法:【注册路由的核心函数】
func (e *Engine) addRoute(method, path string, handlers HandlersChain) {root := e.trees.get(method) // 获取当前方法对应的路由树if root == nil {root = new(node) // 如果路由树不存在,创建新的树e.trees = append(e.trees, methodTree{method: method, root: root})}root.addRoute(path, handlers) // 将路径插入到 Radix 树中
}
Radix 树节点插入逻辑:addRoute in node
在 node 中的 addRoute 方法负责将路径拆分并插入到树中:
// addRoute 将具有给定句柄的节点添加到路径中。
// 不是并发安全的
func (n *node) addRoute(path string, handlers HandlersChain) {fullPath := pathn.priority++numParams := countParams(path) // 数一下参数个数// 空树就直接插入当前节点if len(n.path) == 0 && len(n.children) == 0 {n.insertChild(numParams, path, fullPath, handlers)n.nType = rootreturn}parentFullPathIndex := 0walk:for {// 更新当前节点的最大参数个数if numParams > n.maxParams {n.maxParams = numParams}// 找到最长的通用前缀// 这也意味着公共前缀不包含“:”"或“*” /// 因为现有键不能包含这些字符。i := longestCommonPrefix(path, n.path)// 分裂边缘(此处分裂的是当前树节点)// 例如一开始path是search,新加入support,s是他们通用的最长前缀部分// 那么会将s拿出来作为parent节点,增加earch和upport作为child节点if i < len(n.path) {child := node{path: n.path[i:], // 公共前缀后的部分作为子节点wildChild: n.wildChild,indices: n.indices,children: n.children,handlers: n.handlers,priority: n.priority - 1, //子节点优先级-1fullPath: n.fullPath,}// Update maxParams (max of all children)for _, v := range child.children {if v.maxParams > child.maxParams {child.maxParams = v.maxParams}}n.children = []*node{&child}// []byte for proper unicode char conversion, see #65n.indices = string([]byte{n.path[i]})n.path = path[:i]n.handlers = niln.wildChild = falsen.fullPath = fullPath[:parentFullPathIndex+i]}// 将新来的节点插入新的parent节点作为子节点if i < len(path) {path = path[i:]if n.wildChild { // 如果是参数节点parentFullPathIndex += len(n.path)n = n.children[0]n.priority++// Update maxParams of the child nodeif numParams > n.maxParams {n.maxParams = numParams}numParams--// 检查通配符是否匹配if len(path) >= len(n.path) && n.path == path[:len(n.path)] {// 检查更长的通配符, 例如 :name and :namesif len(n.path) >= len(path) || path[len(n.path)] == '/' {continue walk}}pathSeg := pathif n.nType != catchAll {pathSeg = strings.SplitN(path, "/", 2)[0]}prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.pathpanic("'" + pathSeg +"' in new path '" + fullPath +"' conflicts with existing wildcard '" + n.path +"' in existing prefix '" + prefix +"'")}// 取path首字母,用来与indices做比较c := path[0]// 处理参数后加斜线情况if n.nType == param && c == '/' && len(n.children) == 1 {parentFullPathIndex += len(n.path)n = n.children[0]n.priority++continue walk}// 检查路path下一个字节的子节点是否存在// 比如s的子节点现在是earch和upport,indices为eu// 如果新加一个路由为super,那么就是和upport有匹配的部分u,将继续分列现在的upport节点for i, max := 0, len(n.indices); i < max; i++ {if c == n.indices[i] {parentFullPathIndex += len(n.path)i = n.incrementChildPrio(i)n = n.children[i]continue walk}}// 否则就插入if c != ':' && c != '*' {// []byte for proper unicode char conversion, see #65// 注意这里是直接拼接第一个字符到n.indicesn.indices += string([]byte{c})child := &node{maxParams: numParams,fullPath: fullPath,}// 追加子节点n.children = append(n.children, child)n.incrementChildPrio(len(n.indices) - 1)n = child}n.insertChild(numParams, path, fullPath, handlers)return}// 已经注册过的节点if n.handlers != nil {panic("handlers are already registered for path '" + fullPath + "'")}n.handlers = handlersreturn}
}
整个路由树构造的详细过程:
(1)第一次注册路由,例如注册search
(2)继续注册一条没有公共前缀的路由,例如blog
(3)注册一条与先前注册的路由有公共前缀的路由,例如support
路由注册示例:
package mainimport ("github.com/gin-gonic/gin"
)func main() {r := gin.Default()// 注册静态路由r.GET("/hello", func(c *gin.Context) {c.String(200, "Hello, World!")})// 注册动态路由r.GET("/user/:id", func(c *gin.Context) {id := c.Param("id")c.String(200, "User ID: %s", id)})// 注册通配符路由r.GET("/static/*filepath", func(c *gin.Context) {filepath := c.Param("filepath")c.String(200, "Filepath: %s", filepath)})r.Run(":8080")
}
(4)r.Run()
路由匹配
路由匹配是根据请求路径在 Radix 树中查找对应节点并执行处理函数的过程。
核心代码:getValue
getValue
方法负责在 Radix 树中查找路径:
(1)Run()方法:
/ Run attaches the router to a http.Server and starts listening and serving HTTP requests.
// It is a shortcut for http.ListenAndServe(addr, router)
// Note: this method will block the calling goroutine indefinitely unless an error happens.
func (engine *Engine) Run(addr ...string) (err error) {defer func() { debugPrintError(err) }()if engine.isUnsafeTrustedProxies() {debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +"Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.")}address := resolveAddress(addr)debugPrint("Listening and serving HTTP on %s\n", address)err = http.ListenAndServe(address, engine.Handler())return
}
(2)Handler()方法处理类:
func (engine *Engine) Handler() http.Handler {if !engine.UseH2C {return engine}h2s := &http2.Server{}return h2c.NewHandler(engine, h2s)
}
(3)http.Handler
type Handler interface {ServeHTTP(ResponseWriter, *Request)
}
(4)ServeHTTP实现:
// gin.go
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {// 这里使用了对象池c := engine.pool.Get().(*Context)// 这里有一个细节就是Get对象后做初始化c.writermem.reset(w)c.Request = reqc.reset()engine.handleHTTPRequest(c) // 我们要找的处理HTTP请求的函数engine.pool.Put(c) // 处理完请求后将对象放回池子
}
(5)handleHTTPRequest方法
// gin.go
func (engine *Engine) handleHTTPRequest(c *Context) {// 根据请求方法找到对应的路由树t := engine.treesfor i, tl := 0, len(t); i < tl; i++ {if t[i].method != httpMethod {continue}root := t[i].root// 在路由树中根据path查找value := root.getValue(rPath, c.Params, unescape)if value.handlers != nil {c.handlers = value.handlersc.Params = value.paramsc.fullPath = value.fullPathc.Next() // 执行函数链条c.writermem.WriteHeaderNow()return}c.handlers = engine.allNoRouteserveError(c, http.StatusNotFound, default404Body)
}
(6)getValue
方法
路由匹配是由节点的 getValue方法实现的。getValue根据给定的路径(键)返回nodeValue值,保存注册的处理函数和匹配到的路径参数数据。
如果找不到任何处理函数,则会尝试TSR(尾随斜杠重定向)。
// tree.gotype nodeValue struct {handlers HandlersChainparams Params // []Paramtsr boolfullPath string
}func (n *node) getValue(path string, po Params, unescape bool) (value nodeValue) {value.params = po
walk: // Outer loop for walking the treefor {prefix := n.pathif path == prefix {// 我们应该已经到达包含处理函数的节点。// 检查该节点是否注册有处理函数if value.handlers = n.handlers; value.handlers != nil {value.fullPath = n.fullPathreturn}if path == "/" && n.wildChild && n.nType != root {value.tsr = truereturn}// 没有找到处理函数 检查这个路径末尾+/ 是否存在注册函数indices := n.indicesfor i, max := 0, len(indices); i < max; i++ {if indices[i] == '/' {n = n.children[i]value.tsr = (len(n.path) == 1 && n.handlers != nil) ||(n.nType == catchAll && n.children[0].handlers != nil)return}}return}if len(path) > len(prefix) && path[:len(prefix)] == prefix {path = path[len(prefix):]// 如果该节点没有通配符(param或catchAll)子节点// 我们可以继续查找下一个子节点if !n.wildChild {c := path[0]indices := n.indicesfor i, max := 0, len(indices); i < max; i++ {if c == indices[i] {n = n.children[i] // 遍历树continue walk}}// 没找到// 如果存在一个相同的URL但没有末尾/的叶子节点// 我们可以建议重定向到那里value.tsr = path == "/" && n.handlers != nilreturn}// 根据节点类型处理通配符子节点n = n.children[0]switch n.nType {case param:// find param end (either '/' or path end)end := 0for end < len(path) && path[end] != '/' {end++}// 保存通配符的值if cap(value.params) < int(n.maxParams) {value.params = make(Params, 0, n.maxParams)}i := len(value.params)value.params = value.params[:i+1] // 在预先分配的容量内扩展slicevalue.params[i].Key = n.path[1:]val := path[:end]if unescape {var err errorif value.params[i].Value, err = url.QueryUnescape(val); err != nil {value.params[i].Value = val // fallback, in case of error}} else {value.params[i].Value = val}// 继续向下查询if end < len(path) {if len(n.children) > 0 {path = path[end:]n = n.children[0]continue walk}// ... but we can'tvalue.tsr = len(path) == end+1return}if value.handlers = n.handlers; value.handlers != nil {value.fullPath = n.fullPathreturn}if len(n.children) == 1 {// 没有找到处理函数. 检查此路径末尾加/的路由是否存在注册函数// 用于 TSR 推荐n = n.children[0]value.tsr = n.path == "/" && n.handlers != nil}returncase catchAll:// 保存通配符的值if cap(value.params) < int(n.maxParams) {value.params = make(Params, 0, n.maxParams)}i := len(value.params)value.params = value.params[:i+1] // 在预先分配的容量内扩展slicevalue.params[i].Key = n.path[2:]if unescape {var err errorif value.params[i].Value, err = url.QueryUnescape(path); err != nil {value.params[i].Value = path // fallback, in case of error}} else {value.params[i].Value = path}value.handlers = n.handlersvalue.fullPath = n.fullPathreturndefault:panic("invalid node type")}}// 找不到,如果存在一个在当前路径最后添加/的路由// 我们会建议重定向到那里value.tsr = (path == "/") ||(len(prefix) == len(path)+1 && prefix[len(path)] == '/' &&path == prefix[:len(prefix)-1] && n.handlers != nil)return}
}
Radix 树的路径匹配过程:
package mainimport ("fmt"
)type node struct {path stringchildren []*nodehandlers func()
}func (n *node) addRoute(path string, handler func()) {child := &node{path: path, handlers: handler}n.children = append(n.children, child)
}func (n *node) getRoute(path string) func() {for _, child := range n.children {if child.path == path {return child.handlers}}return nil
}func main() {root := &node{}root.addRoute("/hello", func() {fmt.Println("Hello, World!")})handler := root.getRoute("/hello")if handler != nil {handler() // 输出:Hello, World!} else {fmt.Println("Route not found!")}
}
总结
- 创建路由表:
- 每种 HTTP 方法有独立的 Radix 树。
- 路由通过
addRoute
插入到对应的树中。
- 处理 HTTP 请求:
- Gin 的入口是
Engine
的ServeHTTP
方法。 - 根据请求方法和路径查找路由节点:
- 如果找到,执行绑定的处理函数。
- 如果未找到,执行 404 处理函数。
- Gin 的入口是
- 分发请求:
- 匹配成功的路由节点的处理函数会被依次执行,支持中间件链。
相关文章:

【GO基础学习】gin框架路由详解
文章目录 gin框架路由详解(1)go mod tidy(2)r : gin.Default()(3)r.GET()路由注册 (4)r.Run()路由匹配 总结 gin框架路由详解 先创建一个项目,编写一个简单的demo&#…...
GPIO+TIM(无PWM)实现呼吸灯功能
程序特点: 1、模块化,可快速移植,5分钟便可完成移植。 2、通过GPIO普通定时器,实现呼吸灯功能。 3、PWM周期为5ms,占空比调节时间为20ms,占空比为100等份,即呼吸灯从暗到亮需要20ms*1002s。 …...
贪心算法.
贪心算法是指只从当前角度出发,做出当前情景下最好的选择,在某种意义上来说是局部最优解,并不从全局的角度做决策.如果贪心策略选择不恰当,可能无法得到全局最优解. 贪心算法的基本流程如下: 1.分析问题,确定优化目标,对变量进行初始化 2.制定贪心策略:在制定贪心策略时需要…...
Linux系统和makefile详解
### Linux系统详解 Linux是一个开源且功能强大的操作系统内核,自1991年由林纳斯托瓦兹首次发布以来,它已经成为全球最流行的操作系统之一。Linux的核心特性包括开源、多用户多任务、高稳定性与安全性,以及良好的跨平台能力。 1. **开源**&a…...

GitLab 将停止为中国区用户提供服务,60天迁移期如何应对? | LeetTalk Daily
“LeetTalk Daily”,每日科技前沿,由LeetTools AI精心筛选,为您带来最新鲜、最具洞察力的科技新闻。 GitLab作为一个广受欢迎的开源代码托管平台,近期宣布将停止服务中国大陆、澳门和香港地区的用户提供服务。根据官方通知&#x…...
【杂谈】-AI搜索引擎如何改变传统SEO及其在内容营销中的作用
AI搜索引擎如何改变传统SEO及其在内容营销中的作用 文章目录 AI搜索引擎如何改变传统SEO及其在内容营销中的作用1、什么是AI搜索引擎2、AI搜索引擎对SEO策略的影响3、AI搜索引擎在内容营销转型中的作用4、AI搜索引擎在营销领域的挑战、道德问题和未来5、总结 在当今的数字营销世…...

PTA数据结构编程题7-1最大子列和问题
我参考的B站up的思路 题目 题目链接 给定K个整数组成的序列{ N 1 , N 2 , …, N K },“连续子列”被定义为{ N i , N i1 , …, N j },其中 1≤i≤j≤K。“最大子列和”则被定义为所有连续子列元素的和中最大者。例如给定序列{ -2, 1…...

深入浅出:AWT的基本组件及其应用
目录 前言 1. AWT简介 2. AWT基本组件 2.1 Button:按钮 2.2 Label:标签 编辑 2.3 TextField:文本框 2.4 Checkbox:复选框 2.5 Choice:下拉菜单 2.6 List:列表 综合案例 注意 3. AWT事件处理 …...

MySQL45讲 第三十六讲 为什么临时表可以重名?——阅读总结
文章目录 MySQL45讲 第三十六讲 为什么临时表可以重名?——阅读总结一、引言二、临时表与内存表的区别(一)内存表(二)临时表 三、临时表的特性(一)可见性与生命周期(二)与…...

WebRTC服务质量(11)- Pacer机制(03) IntervalBudget
WebRTC服务质量(01)- Qos概述 WebRTC服务质量(02)- RTP协议 WebRTC服务质量(03)- RTCP协议 WebRTC服务质量(04)- 重传机制(01) RTX NACK概述 WebRTC服务质量(…...
.NET常用的ORM框架及性能优劣分析总结
市面上有很多流行的 ORM(对象关系映射)框架可以用于 .NET 开发。本文主要针对以下几种常见的 ORM 框架,对其优劣进行分析及总结,希望能够帮助大家进行ORM框架的使用有所帮助。 1. Entity Framework (EF) 特点 • 官方支持&…...

Ubuntu网络配置(桥接模式, nat模式, host主机模式)
windows上安装了vmware虚拟机, vmware虚拟机上运行着ubuntu系统。windows与虚拟机可以通过三种方式进行通信。分别是桥接模式;nat模式;host模式 一、桥接模式 所谓桥接模式,也就是虚拟机与宿主机处于同一个网段, 宿主机…...

光通信复习
第一章 1.5 光纤通信系统的基本组成是怎么样的?试画出简图予以说明 光纤:主要负责光信号的传输光发送器:将用户端的电信号转化为光信号,入射到光纤内部光中继器:将光纤中发生衰减和畸变的光信号变成没有衰减和畸变的原…...
数字化转型中的投资决策:IT平台投资与业务应用投资的思考
在数字化转型的大潮中,企业常常面临一个核心问题:如何在繁杂的投资决策中精准地分配资源,特别是在IT平台投资和业务应用投资之间,如何合理划分责任与投入?在一些大型企业中,尤其是华为,针对不同…...

Linux快速入门-Linux的常用命令
Linux的常用命令 1. Linux的终端与工作区1.1 终端概述1.2 切换终端 2. Shell语言解释器2.1 Shell概述 3. 用户登录与身份切换3.1 su 命令3.2 sudo 命令 4. 文件、目录操作命令4.1 pwd 命令4.2 cd 命令4.3 ls 命令4.3.1 ls 指令叠加使用 4.4 mkdir 命令4.5 rmdir 命令4.6 cp 命令…...

【ORB-SLAM3:相机针孔模型和相机K8模型】
在ORB-SLAM3中,相机的建模是 SLAM 系统的核心之一,因为它直接影响到如何处理和利用图像数据进行定位和地图构建。ORB-SLAM3 支持不同的相机模型,其中包括针孔模型和鱼眼模型(K8 模型)。下面分别介绍这两种模型。 相机…...

Python函数(十二):函数的创建和调用、参数传递、返回值
前言:在编程的世界里,函数是一种基本的构建块,它允许我们将代码封装成可重复使用的单元。在Python中,函数的使用尤为重要,因为它不仅有助于代码的模块化,还提高了代码的可读性和可维护性。本章节࿰…...
掌握Docker命令与Dockerfile实战技巧:快速构建高效容器化应用
1. 介绍 Docker 是现代开发和运维的必备工具,集成了容器技术的优势。本文将记录 Docker 的常用指令,并会随着使用经验的积累进行不定期更新。 2. 常用命令 2.1 启动容器(前台交互模式) docker run --privileged --volume /hom…...

Virtualbox硬盘扩容
前言 有没有使用虚拟机安装操作系统的时候,虚拟硬盘一开始分配的虚拟硬盘空间不够用?在后期去扩容的伙伴们,下面我看看如何扩容virtualbox的虚拟硬盘? 重新分配虚拟硬盘大小 在virtualbox菜单选择【管理】-【工具】-【虚拟介质…...

10G光纤反射内存卡
在科技日新月异的今天,数据存储技术正以前所未有的速度发展,其中,“10G光纤反射内存卡”作为新一代存储技术的佼佼者,正逐步引领着数据存储领域的新风尚。本文将深入探讨这一创新产品的技术原理、性能优势、应用场景以及未来展望&…...

剑指offer20_链表中环的入口节点
链表中环的入口节点 给定一个链表,若其中包含环,则输出环的入口节点。 若其中不包含环,则输出null。 数据范围 节点 val 值取值范围 [ 1 , 1000 ] [1,1000] [1,1000]。 节点 val 值各不相同。 链表长度 [ 0 , 500 ] [0,500] [0,500]。 …...
数据链路层的主要功能是什么
数据链路层(OSI模型第2层)的核心功能是在相邻网络节点(如交换机、主机)间提供可靠的数据帧传输服务,主要职责包括: 🔑 核心功能详解: 帧封装与解封装 封装: 将网络层下发…...

IT供电系统绝缘监测及故障定位解决方案
随着新能源的快速发展,光伏电站、储能系统及充电设备已广泛应用于现代能源网络。在光伏领域,IT供电系统凭借其持续供电性好、安全性高等优势成为光伏首选,但在长期运行中,例如老化、潮湿、隐裂、机械损伤等问题会影响光伏板绝缘层…...

9-Oracle 23 ai Vector Search 特性 知识准备
很多小伙伴是不是参加了 免费认证课程(限时至2025/5/15) Oracle AI Vector Search 1Z0-184-25考试,都顺利拿到certified了没。 各行各业的AI 大模型的到来,传统的数据库中的SQL还能不能打,结构化和非结构的话数据如何和…...

论文阅读:Matting by Generation
今天介绍一篇关于 matting 抠图的文章,抠图也算是计算机视觉里面非常经典的一个任务了。从早期的经典算法到如今的深度学习算法,已经有很多的工作和这个任务相关。这两年 diffusion 模型很火,大家又开始用 diffusion 模型做各种 CV 任务了&am…...
【把数组变成一棵树】有序数组秒变平衡BST,原来可以这么优雅!
【把数组变成一棵树】有序数组秒变平衡BST,原来可以这么优雅! 🌱 前言:一棵树的浪漫,从数组开始说起 程序员的世界里,数组是最常见的基本结构之一,几乎每种语言、每种算法都少不了它。可你有没有想过,一组看似“线性排列”的有序数组,竟然可以**“长”成一棵平衡的二…...
Windows 下端口占用排查与释放全攻略
Windows 下端口占用排查与释放全攻略 在开发和运维过程中,经常会遇到端口被占用的问题(如 8080、3306 等常用端口)。本文将详细介绍如何通过命令行和图形化界面快速定位并释放被占用的端口,帮助你高效解决此类问题。 一、准…...
VUE3 ref 和 useTemplateRef
使用ref来绑定和获取 页面 <headerNav ref"headerNavRef"></headerNav><div click"showRef" ref"buttonRef">refbutton</div>使用ref方法const后面的命名需要跟页面的ref值一样 const buttonRef ref(buttonRef) cons…...

RabbitMQ work模型
Work 模型是 RabbitMQ 最基础的消息处理模式,核心思想是 多个消费者竞争消费同一个队列中的消息,适用于任务分发和负载均衡场景。同一个消息只会被一个消费者处理。 当一个消息队列绑定了多个消费者,每个消息消费的个数都是平摊的&a…...

CAD多面体密堆积3D插件
插件介绍 CAD多面体密堆积3D插件可在AutoCAD内建立三维随机多面体密堆积模型。 插件内置物理动力学模拟算法,通过模拟重力、碰撞等现象,使多面体在虚拟环境中发生自然堆积,进而实现真实的堆积效果。多面体堆积模拟中存在的局部穿模问题可通…...