go web框架 gin-gonic源码解读02————router

go web框架 gin-gonic源码解读02————router


本来想先写context,但是发现context能简单讲讲的东西不多,就准备直接和router合在一起讲好了

router是web服务的路由,是指讲来自客户端的http请求与服务器端的处理逻辑或者资源相映射的机制。(这里简单说说,详细的定义网上都可以查到)
那一个优秀的web router应该提供以下功能:
  1. URL解析:路由的过程始于URL解析。URL是一个标识资源位置的字符串,通常由协议、主机名、端口号和路径组成。服务器需要解析这个URL,以便找到对应的处理程序或资源。
  2. 路由规则:在Web应用程序中,通常会定义一组路由规则,这些规则将特定的URL模式与相应的处理程序或资源进行绑定。路由规则可以基于URL路径、HTTP方法(GET、POST等)、查询参数和其他条件来匹配请求。
  3. 动态路由:除了静态路由(固定的URL与处理程序映射)外,现代Web应用程序还支持动态路由。动态路由允许使用参数将URL与处理程序关联起来。这样,同一种类型的请求可以通过不同的参数调用不同的处理程序,实现更灵活和通用的路由。

为了展示gin框架的router的优越性,我们先看看go原生的net/http处理(不是说net/http不好,只是说说他的一些小缺点,不要网暴我) 这里摘抄部分net/http默认的路由器的代码

// GO\src\net\http\server.go// ServeMux就是我们net/http默认的router
type ServeMux struct {mu    sync.RWMutex			   // 大家都知道为了并发安全,搞得读锁 m     map[string]muxEntry // 这个map就是我们路由的核心es    []muxEntry // slice of entries sorted from longest to shortest.// 为了前缀匹配搞得切片,后面func match() 方法你一看就知道他是干什么用的了	hosts bool       // whether any patterns contain hostnames 
}// 上一篇文章说过的每个Server都要实现的ServeHTTP(w ResponseWriter, r *Request)接口会调用这个方法来获取要执行的h Handler(可以理解为这个传参path的这个url对应的逻辑)
func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {mux.mu.RLock()defer mux.mu.RUnlock()// Host-specific pattern takes precedence over generic ones// 本代码段行6的波尔值,这个配置一般是有要区别不同主机会有不同逻辑的时候才会用if mux.hosts {// 这个match就是我们net/http的匹配算法h, pattern = mux.match(host + path)}if h == nil {// 这个match就是我们net/http的匹配算法h, pattern = mux.match(path)}if h == nil {// 默认的404处理返回函数h, pattern = NotFoundHandler(), ""}return
}// 匹配算法 (非常的简单,特别是和后面的gin比起来)
func (mux *ServeMux) match(path string) (h Handler, pattern string) {// Check for exact match first.// 很简单直接去map里find,其实效率也还可以,但是这里你就能看出来他只能支持静态路由,map这里也不支持模糊搜索v, ok := mux.m[path]if ok {// return v.h, v.pattern}// Check for longest valid match.  mux.es contains all patterns// that end in / sorted from longest to shortest.// 这里英文注释也蛮清楚的,就是map里找不到,这里找一下以入参path为前缀的url。// 并且这个mux.es还是有序的,为了提升一点效率,从这里看他似乎也不是完全静态的。for _, e := range mux.es {if strings.HasPrefix(path, e.pattern) {return e.h, e.pattern}}return nil, ""
}

这里篇幅有限只讲讲net/http的match,inster就不说了。

从上面的macth代码也可以看出net/http的路由存在以下缺点

  1. 缺乏灵活的路由定义:net/http 包的路由定义相对简单,只能通过 http.HandleFunc 或 http.Handle 来定义路由处理函数。这导致难以支持复杂的路由模式,如正则表达式匹配、参数提取等

  2. 不支持动态路由:这个包并没有原生支持动态路由,即不能直接将路由和参数绑定起来,需要手动解析 URL。

  3. 不支持中间件:中间件是一种常用的扩展路由处理的方法,可以在请求到达路由处理函数之前或之后执行一些操作。然而,net/http 包没有内置的中间件支持,需要手动编写和管理中间件。

  4. 不支持子路由:在一些应用场景中,需要将不同类型的请求映射到不同的处理函数,这些处理函数可能共享某些共同的前缀。net/http 包并没有内置的子路由支持,需要自己实现。

  5. 不支持路由组:在一些情况下,希望将一组相关的路由规则进行分组管理,以便更好地组织代码。net/http 包没有原生支持路由组的功能。

接下来正片开始,讲router主要讲两个函数:match(路由匹配),insert(路由注册)

gin的路由数据结构

和大多说的web框架一样,gin选择了使用前缀树算法,来进行路由匹配,因为确实蛮合适的。前缀树这边不细讲了,蛮简单的,大家可Google看看。这里直接撸gin的源码。


// ../gin/tree.go// static:表示静态节点。静态节点是指路由路径中没有参数或通配符的节点,其值是固定的字符串。例如,路径 "/home" 中的 "home" 就是一个静态节点。
// root:表示根节点。根节点是整个路由树的顶级节点,它没有路径,其作用是起始点,用于构建路由树的根结构。
// param:表示参数节点。参数节点是指路由路径中的一部分可以是变量的节点,例如 "/user/:id" 中的 ":id" 就是一个参数节点,可以匹配任意值。
// catchAll:表示通配符节点。通配符节点是指路由路径中的一部分可以匹配任意内容的节点,例如 "/static/*filepath" 中的 "*filepath" 就是一个通配符节点,可以匹配以 "/static/" 开头的所有路径。type nodeType uint8const (static nodeType = iotarootparamcatchAll
)
// 这个结构存放的是每个方法的根节点,如GET,POST,PUT,他们的根节点也是这种方式在Engine中存储的
// type Engine struct {
// 	...
//  // 简单来书就是每种请求方法是一颗独立的前缀树
// 	trees            methodTrees
// 	...
// }
type methodTrees []methodTree
// 一个简单的get方法
func (trees methodTrees) get(method string) *node {for _, tree := range trees {if tree.method == method {return tree.root}}return nil
}type methodTree struct {method stringroot   *node
}// 树的各个节点
type node struct {// 到该节点的路由路径片段,例如"/home",那他就是hemopath      string// 索引,下文细讲indices   string// 子节点中,是否是有通配符节点,有的话插入新的节点时,要注意维护通配符节点是最后一个节点wildChild bool// 上文提到的该节点的类型nType     nodeType// 优先级,下文细讲priority  uint32// 子节点,gin是以字符串的每个字符当做节点的children  []*node// 所有的中间件和对应的url处理的逻辑函数,后续讲中间件的时候细讲为什么中间件和服务逻辑函数写在一个数组里handlers  HandlersChain// 全路径fullPath  string
}

路由器的节点插入(路由绑定)

使用过gin框架的同学都知道,我们在使用go-gin时,只需要 gin.GET(“/hello/world”, helloworld) 这么一句简单的代码就可以实现url"/hello/world" 和 逻辑函数helloworld()的绑定,接下来让我们看看func GET()里都发生了什么。

func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers ...HandlerFunc) IRoutes {if matched := regEnLetter.MatchString(httpMethod); !matched {panic("http method " + httpMethod + " is not valid")}return group.handle(httpMethod, relativePath, handlers)
}// POST is a shortcut for router.Handle("POST", path, handlers).
func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes {return group.handle(http.MethodPost, relativePath, handlers)
}// 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)
}

当然不止get和post,gin还定义各种其他的http的请求方法,但是都大同小异,这边以get和post举例。
从这里可以看出来不管是什么http的方法最终调用的都是Handle 方法,并且讲请求方法作以string的方式传入。比如注释上说的
// POST is a shortcut for router.Handle(“POST”, path, handlers)

func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers …HandlerFunc) IRoutes {}

而Handle()方法之中又调用了一个handle方法

// gin.gofunc (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {// (group *RouterGroup) RouterGroup 大家常用也很熟悉,就是我们的url分组,我们开发中会为每个url分组设置一个统一的前缀url,// group.calculateAbsolutePath(relativePath)这一步是为了帮助我们拼接处正确的url全文absolutePath := group.calculateAbsolutePath(relativePath)// handlers 就是我们这篇文章上面所讲的node 结构体中的handlers 。// 这一步是为了把你注册路由绑定的服务逻辑方法绑定到调用链上,这个下一章讲中间件的时候会细讲handlers = group.combineHandlers(handlers)// 终于到了最终的一步!AddRouter,这个方法里就是我们路由器的节点插入(路由绑定)核心方法group.engine.addRoute(httpMethod, absolutePath, handlers)return group.returnObj()
}// 核心方法 addRoute (这里为了篇幅我会做一些摘抄)
func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {// 做一些断言,检测一下输入的url是否合法,比较越早阶段的暴露问题是程序的铁律assert1(path[0] == '/', "path must begin with '/'")assert1(method != "", "HTTP method can not be empty")assert1(len(handlers) > 0, "there must be at least one handler")// debug模式下一些日志打印(不太重要)debugPrintRoute(method, path, handlers)// engine.tree中存储了一个http请求方法的字符串为根节点的切片,所以这里我们拿http请求方法method先get// 这个get方法在上文有,大家可以自己拉上去看看root := engine.trees.get(method)// 一个好的设计肯定不可能一开始把所有的可能性都构造出来,这边也是,管你是get还是post,都是用到了再创建,然后插入。if root == nil {root = new(node)root.fullPath = "/"// 这种http请求方法是第一次出现,这边创建一个,并且插入engine.trees = append(engine.trees, methodTree{method: method, root: root})}// 核心的核心来了,大家请看下面的代码块tree.goroot.addRoute(path, handlers)// Update maxParamsif paramsCount := countParams(path); paramsCount > engine.maxParams {engine.maxParams = paramsCount}if sectionsCount := countSections(path); sectionsCount > engine.maxSections {engine.maxSections = sectionsCount}
}

tree.go

// tree.go// 核心的核心非常的长
func (n *node) addRoute(path string/*绑定的url路径*/, handlers HandlersChain/*绑定的逻辑函数*/) {fullPath := path// 优先级++,主要用于每个节点子节点排序,提高查找的效率n.priority++// Empty treeif len(n.path) == 0 && len(n.children) == 0 {n.insertChild(path, fullPath, handlers)n.nType = rootreturn}parentFullPathIndex := 0walk:for {// Find the longest common prefix.// This also implies that the common prefix contains no ':' or '*'// since the existing key can't contain those chars.// 大家注意哈,我们这里进循环了,请开始转动你的小脑瓜// 就是找当前节点的路径和你patch最长的相同的前缀,并且放回下标i := longestCommonPrefix(path, n.path)// Split edge// 显然现在的path和该节点的path有不相同的地方,说明我们的前缀树要开始分叉了// 接下来我们要做的就是把我们传入的path从不相同的地方开始拆分成两个节点。if i < len(n.path) {// 创建一个孩子节点, 初始化的时候我们先把大部分的值设置的和我们的当前节点一致child := node{// 这里就是我说的拆分的地方,你看从两者字符串不同的地方的下标剪断了path:      n.path[i:],wildChild: n.wildChild,nType:     static,indices:   n.indices,children:  n.children,handlers:  n.handlers,// 这个开始拆分就表示他至少会有两个子节点,优先级就降低了priority:  n.priority - 1,fullPath:  n.fullPath,}// 由于一个分支下的所有孩子节点都会有相同的前缀,也就是他们父节点所记录的值,这里字符出现了不同// 其实就是我们当前节点成了父节点,我们要把相同的部分留给当前节点,然后不同部分分成两个新的子节点// 所以这里先把当前节点拆了,拆一个子节点出来。n.children = []*node{&child}// []byte for proper unicode char conversion, see #65// 这段代码看完也基本解开了indices这个索引变量的神秘面纱了,他其实就是该节点的所有字节的首字符// 拼接在一起的一个字符串,方便后面的查找,这里只有一个i是因为他把当前节点拆成了0-i和i-len两个节点// 在我们path插入之前他肯定只有一个孩子节点。n.indices = bytesconv.BytesToString([]byte{n.path[i]})n.path = path[:i]// 当前节点现在被拆分了,他现在只是前缀树上的一个分叉节点,它本身并不代表某个url,所以给他的这两个参数置为nil和falsen.handlers = niln.wildChild = falsen.fullPath = fullPath[:parentFullPathIndex+i]}// Make new node a child of this node// 当前节点虽然已经被拆成了两个节点:父节点(当前node与我们字符匹配的部分)--> 子节点(当前node与我们字符不匹配的部分)// 当时我们自己的path还没有变成节点插入呢。这里会有两种情况,一种是字符串匹配下标i小于path的长度或者大于等于path的长度// 这里我们分开处理,先说 i < len(path) 这种情况下我们tree会是下图这种情况// 父节点(当前node与我们字符匹配的部分)// |--> 子节点1(当前node与我们字符不匹配的部分)// |--> 字节点2(我们的path)// 这里其实就两种情况。1.i<len(path);2.i=len(path);1情况下面还有几种情况要处理// 而情况2相当于当前节点就是我们的path了,需要给他绑定逻辑函数handlersif i < len(path) {// path的前半段path[0-i]已经被当前节点node表示,所以这里裁掉path = path[i:]c := path[0]// '/' after param// 这里处理一种特殊情况,当前节点是param参数节点,且他只有一个子节点,并且我们用来查找的字符串是'/'// 那就这个节点这里两者是可以匹配上的,那我们直接把当前节点变成子节点继续匹配。if n.nType == param && c == '/' && len(n.children) == 1 {parentFullPathIndex += len(n.path)// continue了,请大家带着当前所有的记忆返回 代码行walk:n = n.children[0]n.priority++continue walk}// Check if a child with the next path byte exists// 遍历当前节点的所有索引,其实就是看看孩子节点里有没有哪个首字符和我们path首字符一样的for i, max := 0, len(n.indices); i < max; i++ {if c == n.indices[i] {parentFullPathIndex += len(n.path)// 有的话这个孩子就是我们的当前节点了,所以我们要维护一下这个children节点,并且再次拿到他的下标// 维护的方法incrementChildPrio()这个的代码我贴在下面了i = n.incrementChildPrio(i)n = n.children[i]// continue了,请大家带着当前所有的记忆返回 代码行walk:continue walk}}// Otherwise insert it// 走到这一步,说明当前节点的所有的子节点都没有匹配上,我们先看看要插入的path是否是匹配路径(c != ':' && c != '*')// 再看看我们的当前节点是否是匹配节点(n.nType != catchAll)if c != ':' && c != '*' && n.nType != catchAll {// 如果都不是,那说明是个正常节点和正常path,那我们把这个path当这个正常子节点,先给他创造结构体,后续统一插入// []byte for proper unicode char conversion, see #65// 更新当前节点的索引n.indices += bytesconv.BytesToString([]byte{c})// 创建结构体,等待后续插入child := &node{fullPath: fullPath,}// 给当前节点插入子节点n.addChild(child)// 维护索引n.indices有序n.incrementChildPrio(len(n.indices) - 1)n = child} else if n.wildChild {// 到这一步说明当前节点的子节点中有通配符节点,那我们直接取子节点的最后一个节点// (插入子节点的时候我们会特意维护,通配符节点的最后一个,这样子取用起来也很方便)// inserting a wildcard node, need to check if it conflicts with the existing wildcardn = n.children[len(n.children)-1]n.priority++// Check if the wildcard matches// 检查n是否和path匹配(这里n = n.children[len(n.children)-1]了)if len(path) >= len(n.path) && n.path == path[:len(n.path)] &&// Adding a child to a catchAll is not possiblen.nType != catchAll &&// Check for longer wildcard, e.g. :name and :names(len(n.path) >= len(path) || path[len(n.path)] == '/') {// 匹配上了我们直接continue 整个再来一次continue walk}// Wildcard conflict// 这都没匹配上,说明出问题了,这里拼接一下错误,panic了// 一般是同一级分支下出现了两个同级的通配符 ,示例可以看下文的func TestTreePanic1(t *testing.T)pathSeg := pathif n.nType != catchAll {pathSeg = strings.SplitN(pathSeg, "/", 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 +"'")}// 这里说明c是通配符(c == ':' || c == '*')// 或n是全匹配节点(n.nType != catchAll)// 并且都当前节点没有通配符子节点,直接插入n.insertChild(path, fullPath, handlers)return}// Otherwise add handle to current nodeif n.handlers != nil {panic("handlers are already registered for path '" + fullPath + "'")}// 迭代到这里了就是当前节点n就是我们path了,需要最后做一些赋值n.handlers = handlersn.fullPath = fullPathreturn}
}func longestCommonPrefix(a, b string) int {i := 0max := min(len(a), len(b))for i < max && a[i] == b[i] {i++}return i
}// Increments priority of the given child and reorders if necessary
// 这里主要做了一个排序操作,维护了一下node的子节点切片,使他们以priority的 大小为规则排序
// 排序之后我们要拿的那个儿子节点的下标有可能会改变,所以还要返回一个维护过得newpos来保证返回的是正确的坐标
func (n *node) incrementChildPrio(pos int) int {cs := n.children// 优先级现先++cs[pos].priority++prio := cs[pos].priority// Adjust position (move to front)newPos := pos// 经典冒泡,根据优先级priority进行排序for ; newPos > 0 && cs[newPos-1].priority < prio; newPos-- {// Swap node positionscs[newPos-1], cs[newPos] = cs[newPos], cs[newPos-1]}// Build new index char stringif newPos != pos {n.indices = n.indices[:newPos] + // Unchanged prefix, might be emptyn.indices[pos:pos+1] + // The index char we moven.indices[newPos:pos] + n.indices[pos+1:] // Rest without char at 'pos'}return newPos
}
func findWildcard(path string) (wildcard string, i int, valid bool) {// Find startfor start, c := range []byte(path) {// A wildcard starts with ':' (param) or '*' (catch-all)if c != ':' && c != '*' {continue}// Find end and check for invalid charactersvalid = truefor end, c := range []byte(path[start+1:]) {switch c {case '/':return path[start : start+1+end], start, validcase ':', '*':valid = false}}return path[start:], start, valid}return "", -1, false
}
func (n *node) insertChild(path string, fullPath string, handlers HandlersChain) {for {// Find prefix until first wildcard// 这个就是查找path是否有通配符的'*',':','/'// 没有查到直接break// wildcard拿的是中间那段比如:/:w/hello 那wildcard就是:wwildcard, i, valid := findWildcard(path)if i < 0 { // No wildcard foundbreak}// The wildcard name must only contain one ':' or '*' characterif !valid {panic("only one wildcard per path segment is allowed, has: '" +wildcard + "' in path '" + fullPath + "'")}// check if the wildcard has a nameif len(wildcard) < 2 {panic("wildcards must be named with a non-empty name in path '" + fullPath + "'")}// 如果wildcard首字符是':'拼装child,把wildcard之前的不是通配符区块的,拼接到n的path中if wildcard[0] == ':' { // paramif i > 0 {// Insert prefix before the current wildcardn.path = path[:i]path = path[i:]}child := &node{nType:    param,path:     wildcard,fullPath: fullPath,}n.addChild(child)n.wildChild = truen = childn.priority++// if the path doesn't end with the wildcard, then there// will be another subpath starting with '/'if len(wildcard) < len(path) {path = path[len(wildcard):]child := &node{priority: 1,fullPath: fullPath,}n.addChild(child)n = childcontinue}// Otherwise we're done. Insert the handle in the new leafn.handlers = handlersreturn}// catchAllif i+len(wildcard) != len(path) {panic("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'")}if len(n.path) > 0 && n.path[len(n.path)-1] == '/' {pathSeg := strings.SplitN(n.children[0].path, "/", 2)[0]panic("catch-all wildcard '" + path +"' in new path '" + fullPath +"' conflicts with existing path segment '" + pathSeg +"' in existing prefix '" + n.path + pathSeg +"'")}// currently fixed width 1 for '/'i--if path[i] != '/' {panic("no / before catch-all in path '" + fullPath + "'")}n.path = path[:i]// First node: catchAll node with empty pathchild := &node{wildChild: true,nType:     catchAll,fullPath:  fullPath,}n.addChild(child)n.indices = string('/')n = childn.priority++// second node: node holding the variablechild = &node{path:     path[i:],nType:    catchAll,handlers: handlers,priority: 1,fullPath: fullPath,}n.children = []*node{child}return}// If no wildcard was found, simply insert the path and handlen.path = pathn.handlers = handlersn.fullPath = fullPath
}


这里放一些panic的或者正常的测试代码实例方便大家理解

func TestTreePanic1(t *testing.T) {tree := &node{}routes := [...]string{"/hi/","/hi/:go","/hi/:go1",}for _, route := range routes {tree.addRoute(route, fakeHandler(route))}
}

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

react实现对数组做增删改操作自定义hook

需求 实现对数组的增删改操作。 实现 import { useState } from react;const useArray (currList) > {const [list, setList] useState(currList);// 增const addItem (item) > {setList([...list, item]);};// 删const removeItem (idx) > {const _arr [...l…

实战指南,SpringBoot + Mybatis 如何对接多数据源

系列文章目录 MyBatis缓存原理 Mybatis plugin 的使用及原理 MyBatisSpringboot 启动到SQL执行全流程 数据库操作不再困难&#xff0c;MyBatis动态Sql标签解析 从零开始&#xff0c;手把手教你搭建Spring Boot后台工程并说明 Spring框架与SpringBoot的关联与区别 Spring监听器…

轻松解决docker容器启动闪退

docker run -p 3306:3306 --name mysql8 \ -v /usr/local/mysql/log:/var/log/mysql \ -v /usr/local/mysql/data:/var/lib/mysql \ -v /usr/local/mysql/conf:/etc/mysql \ -e MYSQL_ROOT_PASSWORD666 -d mysql:8.0.32执行这个命令的时候闪退&#xff0c;其实这个是命令是对你…

[cv] stable diffusion——2、公式

背景&#xff1a; 在图像生成领域中&#xff0c;最常见的生成模型是GAN和VAE。然而&#xff0c;在2020年&#xff0c;提出了一种新的模型&#xff0c;即DDPM&#xff08;Denoising Diffusion Probabilistic Model&#xff09;&#xff0c;也被称为扩散模型&#xff08;Diffusi…

基于eBPF技术构建一种应用层网络管控解决方案

引言 随着网络应用的不断发展&#xff0c;在linux系统中对应用层网络管控的需求也日益增加&#xff0c;而传统的iptables、firewalld等工具难以针对应用层进行网络管控。因此需要一种创新的解决方案来提升网络应用的可管理性。 本文将探讨如何使用eBPF技术构建一种应用层网络…

【CSS】禁用元素鼠标事件(例如实现元素禁用效果)

文章目录 基本用法 基本用法 pointer-events 属性指定在什么情况下 (如果有) 某个特定的图形元素可以成为鼠标事件。实际运用中可以通过对auto 和none动态控制&#xff0c;来动态实现元素的禁用效果。 属性描述auto与pointer-events属性未指定时的表现效果相同&#xff0c;对…

【笔试题心得】排序算法总结整理

排序算法汇总 常用十大排序算法_calm_G的博客-CSDN博客 以下动图参考 十大经典排序算法 Python 版实现&#xff08;附动图演示&#xff09; - 知乎 冒泡排序 排序过程如下图所示&#xff1a; 比较相邻的元素。如果第一个比第二个大&#xff0c;就交换他们两个。对每一对相邻…

【LeetCode-简单】剑指 Offer 29. 顺时针打印矩阵(详解)

题目 输入一个矩阵&#xff0c;按照从外向里以顺时针的顺序依次打印出每一个数字。 示例 1&#xff1a; 输入&#xff1a;matrix [[1,2,3],[4,5,6],[7,8,9]] 输出&#xff1a;[1,2,3,6,9,8,7,4,5]示例 2&#xff1a; 输入&#xff1a;matrix [[1,2,3,4],[5,6,7,8],[9,10,1…

互联网发展历程:速度与效率,交换机的登场

互联网的演进就像一场追求速度与效率的竞赛&#xff0c;每一次的技术升级都为我们带来更快、更高效的网络体验。然而&#xff0c;在网络的初期阶段&#xff0c;人们面临着数据传输速度不够快的问题。一项关键的技术应运而生&#xff0c;那就是“交换机”。 速度不足的困境&…

CloudEvents—云原生事件规范

我们的系统中或多或少都会用到如下两类业务技术&#xff1a; 异步任务&#xff0c;用于降低接口时延或削峰&#xff0c;提升用户体验&#xff0c;降低系统并发压力&#xff1b;通知类RPC&#xff0c;用于微服务间状态变更&#xff0c;用户行为的联动等场景&#xff1b; 以上两种…

Go和Java实现解释器模式

Go和Java实现解释器模式 下面通过一个四则运算来说明解释器模式的使用。 1、解释器模式 解释器模式提供了评估语言的语法或表达式的方式&#xff0c;它属于行为型模式。这种模式实现了一个表达式接口&#xff0c;该接口 解释一个特定的上下文。这种模式被用在 SQL 解析、符…

规划性和可扩展性,助力企业全面预算管理的推进

对于当今社会经济市场的不稳定状况和不断变化的消费者行为&#xff0c;企业业务也从未像今天这样不可预测过。面对变化和变革&#xff0c;企业需要具备规划性的预测能力&#xff0c;才能使得自身在竞争中保持领先地位。那些具备前瞻性的企业都尝试在现阶段通过更好的规划不断提…

基于Mysqlrouter+MHA+keepalived实现高可用半同步 MySQL Cluster项目

目录 项目名称&#xff1a; 基于Mysqlrouter MHA keepalived实现半同步主从复制MySQL Cluster MySQL Cluster&#xff1a; 项目架构图&#xff1a; 项目环境&#xff1a; 项目环境安装包&#xff1a; 项目描述&#xff1a; 项目IP地址规划&#xff1a; 项目步骤: 一…

windows11下配置vscode中c/c++环境

本文默认已经下载且安装好vscode&#xff0c;主要是解决环境变量配置以及编译task、launch文件的问题。 自己尝试过许多博客&#xff0c;最后还是通过这种方法配置成功了。 Linux(ubuntu 20.04)配置vscode可以直接跳转到配置task、launch文件&#xff0c;不需要下载mingw与配…

宽度有限搜索BFS搜索数及B3625 迷宫寻路 P1451 求细胞数量 B3626 跳跃机器人

宽度有限搜索BFS搜索 B3625 迷宫寻路 题面 题目描述 机器猫被困在一个矩形迷宫里。 迷宫可以视为一个 nm 矩阵&#xff0c;每个位置要么是空地&#xff0c;要么是墙。机器猫只能从一个空地走到其上、下、左、右的空地。 机器猫初始时位于 (1,1) 的位置&#xff0c;问能否…

localhost:8080 is already in use

报错原因&#xff1a;本机的8080端口号已经被占用。因为机器的空闲端口号是随机分配的&#xff0c;而idea默认启动的端口号是8080,所以是存在这种情况。 对于这个问题&#xff0c;我们只需要重启idea或者修改项目的启动端口号即可。 更推荐第二种。对于修改项目启动端口号&…

Python 程序设计入门(020)—— 循环结构程序设计(1):for 循环

Python 程序设计入门&#xff08;020&#xff09;—— 循环结构程序设计&#xff08;1&#xff09;&#xff1a;for 循环 目录 Python 程序设计入门&#xff08;020&#xff09;—— 循环结构程序设计&#xff08;1&#xff09;&#xff1a;for 循环一、for 循环的语法二、for …

ZDH-wemock模块

本次介绍基于版本v5.1.1 目录 项目源码 预览地址 安装包下载地址 wemock模块 wemock模块前端 配置首页 配置mock wemock服务 下载地址 打包 运行 效果展示 项目源码 zdh_web: https://github.com/zhaoyachao/zdh_web zdh_mock: https://github.com/zhaoyachao/z…

TCGA数据下载推荐:R语言easyTCGA包

#使用easyTCGA获取数据 #清空 rm(listls()) gc() # 安装bioconductor上面的R包 options(BioC_mirror"https://mirrors.tuna.tsinghua.edu.cn/bioconductor") if(!require("BiocManager")) install.packages("BiocManager") if(!require("TC…

怎样让音频速度变慢?请跟随以下方法进行操作

怎样让音频速度变慢&#xff1f;在会议录音过程中&#xff0c;经常会遇到主讲人语速过快&#xff0c;导致我们无法清晰听到对方说的内容。如果我们能够减慢音频速度&#xff0c;就能更好地记录对方的讲话内容。此外&#xff0c;在听到快速播放的外语或方言时&#xff0c;我们也…