【GO基础学习】gin框架路由详解

文章目录

  • gin框架路由详解
    • (1)go mod tidy
    • (2)r := gin.Default()
    • (3)r.GET()
      • 路由注册
    • (4)r.Run()
      • 路由匹配
    • 总结


gin框架路由详解

先创建一个项目,编写一个简单的demo,对这个demo进行讲解。

  1. 创建一个go的项目,采用GoLand:

在这里插入图片描述
2. 在该项目下创建一个main.go文件:

package main

import (
	"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 {
    RouterGroup
    trees       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 string
	root   *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 := path
	n.priority++
	numParams := countParams(path)  // 数一下参数个数

	// 空树就直接插入当前节点
	if len(n.path) == 0 && len(n.children) == 0 {
		n.insertChild(numParams, path, fullPath, handlers)
		n.nType = root
		return
	}

	parentFullPathIndex := 0

walk:
	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, //子节点优先级-1
				fullPath:  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 #65
			n.indices = string([]byte{n.path[i]})
			n.path = path[:i]
			n.handlers = nil
			n.wildChild = false
			n.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 node
				if numParams > n.maxParams {
					n.maxParams = numParams
				}
				numParams--

				// 检查通配符是否匹配
				if len(path) >= len(n.path) && n.path == path[:len(n.path)] {
					// 检查更长的通配符, 例如 :name and :names
					if len(n.path) >= len(path) || path[len(n.path)] == '/' {
						continue walk
					}
				}

				pathSeg := path
				if n.nType != catchAll {
					pathSeg = strings.SplitN(path, "/", 2)[0]
				}
				prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path
				panic("'" + 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.indices
				n.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 = handlers
		return
	}
}

整个路由树构造的详细过程:

(1)第一次注册路由,例如注册search
(2)继续注册一条没有公共前缀的路由,例如blog
(3)注册一条与先前注册的路由有公共前缀的路由,例如support

路由注册示例:

package main

import (
    "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 = req
	c.reset()

	engine.handleHTTPRequest(c)  // 我们要找的处理HTTP请求的函数

	engine.pool.Put(c)  // 处理完请求后将对象放回池子
}

(5)handleHTTPRequest方法

// gin.go
func (engine *Engine) handleHTTPRequest(c *Context) {
	// 根据请求方法找到对应的路由树
	t := engine.trees
	for 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.handlers
			c.Params = value.params
			c.fullPath = value.fullPath
			c.Next()  // 执行函数链条
			c.writermem.WriteHeaderNow()
			return
		}
	
	c.handlers = engine.allNoRoute
	serveError(c, http.StatusNotFound, default404Body)
}

(6)getValue方法
路由匹配是由节点的 getValue方法实现的。getValue根据给定的路径(键)返回nodeValue值,保存注册的处理函数和匹配到的路径参数数据。
如果找不到任何处理函数,则会尝试TSR(尾随斜杠重定向)。

// tree.go

type nodeValue struct {
	handlers HandlersChain
	params   Params  // []Param
	tsr      bool
	fullPath string
}

func (n *node) getValue(path string, po Params, unescape bool) (value nodeValue) {
	value.params = po
walk: // Outer loop for walking the tree
	for {
		prefix := n.path
		if path == prefix {
			// 我们应该已经到达包含处理函数的节点。
			// 检查该节点是否注册有处理函数
			if value.handlers = n.handlers; value.handlers != nil {
				value.fullPath = n.fullPath
				return
			}

			if path == "/" && n.wildChild && n.nType != root {
				value.tsr = true
				return
			}

			// 没有找到处理函数 检查这个路径末尾+/ 是否存在注册函数
			indices := n.indices
			for 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.indices
				for i, max := 0, len(indices); i < max; i++ {
					if c == indices[i] {
						n = n.children[i] // 遍历树
						continue walk
					}
				}

				// 没找到
				// 如果存在一个相同的URL但没有末尾/的叶子节点
				// 我们可以建议重定向到那里
				value.tsr = path == "/" && n.handlers != nil
				return
			}

			// 根据节点类型处理通配符子节点
			n = n.children[0]
			switch n.nType {
			case param:
				// find param end (either '/' or path end)
				end := 0
				for 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] // 在预先分配的容量内扩展slice
				value.params[i].Key = n.path[1:]
				val := path[:end]
				if unescape {
					var err error
					if 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't
					value.tsr = len(path) == end+1
					return
				}

				if value.handlers = n.handlers; value.handlers != nil {
					value.fullPath = n.fullPath
					return
				}
				if len(n.children) == 1 {
					// 没有找到处理函数. 检查此路径末尾加/的路由是否存在注册函数
					// 用于 TSR 推荐
					n = n.children[0]
					value.tsr = n.path == "/" && n.handlers != nil
				}
				return

			case 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] // 在预先分配的容量内扩展slice
				value.params[i].Key = n.path[2:]
				if unescape {
					var err error
					if 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.handlers
				value.fullPath = n.fullPath
				return

			default:
				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 main

import (
    "fmt"
)

type node struct {
    path     string
    children []*node
    handlers 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!")
    }
}

总结

  1. 创建路由表
    • 每种 HTTP 方法有独立的 Radix 树。
    • 路由通过 addRoute 插入到对应的树中。
  2. 处理 HTTP 请求
    • Gin 的入口是 EngineServeHTTP 方法。
    • 根据请求方法和路径查找路由节点:
      • 如果找到,执行绑定的处理函数。
      • 如果未找到,执行 404 处理函数。
  3. 分发请求
    • 匹配成功的路由节点的处理函数会被依次执行,支持中间件链。

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

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

相关文章

直流无刷电机驱动原理1--简介和例程演示

基础知识 BLDC&#xff08;Brushless DC Motor&#xff0c;无刷直流电机&#xff09; 和 PMSM&#xff08;Permanent Magnet Synchronous Motor&#xff0c;永磁同步电机&#xff09; 都是基于永磁体技术的无刷电机&#xff0c;但它们在结构、控制方式和应用场景上存在一些区别…

qt5.12.11+msvc编译器编译qoci驱动

1.之前编译过minGW编译器编译qoci驱动,很顺利就完成了,文章地址:minGW编译qoci驱动详解,今天按照之前的步骤使用msvc编译器进行编译,直接就报错了: 查了些资料,发现两个编译器在编译时,pro文件中引用的库不一样,下面是msvc编译器引用的库,其中编译引用的库我这里安装…

【C++动态规划】1458. 两个子序列的最大点积|1823

本文涉及知识点 下载及打开打包代码的方法兼述单元测试 C动态规划 LeetCode1458. 两个子序列的最大点积 LeetCode3290 和此题几乎相同。 给你两个数组 nums1 和 nums2 。 请你返回 nums1 和 nums2 中两个长度相同的 非空 子序列的最大点积。 数组的非空子序列是通过删除原数…

yolov4算法及其改进

yolov4算法及其改进 1、yolov4介绍2、mosaic与mish激活函数2.1、mosaic数据增强2.2、Mish激活函数3、backbone网络框架的改进4、PAN-FPN的介绍5、样本匹配和损失函数1、yolov4介绍 改进点: 输入端改进:Mosaic数据增加主干网络:CSPDarkNet53Neck:SPP、PANet损失函数:CIOU激活…

Astherus 联手 PancakeSwap 推出 asCAKE,CAKE 最大化收益的最优解?

Astherus 是本轮市场周期中最具创新性的 DeFi 协议之一&#xff0c;其通过推出 AstherusEx 以及 AstherusEarn 两个产品&#xff0c;正在基于真实收益启动 DeFi 市场的增长&#xff0c;并成为加密投资者捕获收益的最佳协议。PancakeSwap 是 BNB Chain 上最大的 DEX&#xff0c;…

创意无限!利用Cpolar和Flux.1实现远程AI图像生成功能

文章目录 前言1. 本地部署ComfyUI2. 下载 Flux.1 模型3. 下载CLIP模型4. 下载 VAE 模型5. 演示文生图6. 公网使用 Flux.1 大模型6.1 创建远程连接公网地址7. 固定远程访问公网地址前言 Flux.1 是一款免费开源的图像生成模型,通过ComfyUI,你可以轻松调用这款强大的工具。Flux…

谷歌浏览器 Chrome 提示:此扩展程序可能很快将不再受支持

问题现象 在Chrome 高版本上的扩展管理页面&#xff08;地址栏输入chrome://extensions/或者从界面进入&#xff09;&#xff1a; &#xff0c; 可以查看到扩展的情况。 问题现象大致如图: 问题原因 出现此问题的根本原因在于&#xff1a;谷歌浏览器本身的扩展机制发生了…

关于开机挺快的,但是登录界面输入密码后,卡了许久许久

首先说我的结论&#xff1a;清理一下temp缓存就ok了 这样之后后打开一个文件夹&#xff0c;把里面可以删的东西全删了就行&#xff0c;但是我的太多了&#xff0c;出现了未响应的情况。所以这里贴上一个用cmd删的方法。 rmdir 删除整个目录 好比说我要删除 222 这个目录下的所…

JVM实战—2.JVM内存设置与对象分配流转

大纲 1.JVM内存划分的原理细节 2.对象在JVM内存中如何分配如何流转 3.部署线上系统时如何设置JVM内存大小 4.如何设置JVM堆内存大小 5.如何设置JVM栈内存与永久代大小 6.问题汇总 1.JVM内存划分的原理细节 (1)背景引入 (2)大部分对象的存活周期都是极短的 (3)少数对象…

5G -- 5G网络架构

5G组网场景 从4G到5G的网络演进&#xff1a; 1、UE -> 4G基站 -> 4G核心网 * 部署初中期&#xff0c;利用存量网络&#xff0c;引入5G基站&#xff0c;4G与5G基站并存 2、UE -> (4G基站、5G基站) -> 4G核心网 * 部署中后期&#xff0c;引入5G核心网&am…

8086汇编(16位汇编)学习笔记05.asm基础语法和串操作

8086汇编(16位汇编)学习笔记05.asm基础语法和串操作-C/C基础-断点社区-专业的老牌游戏安全技术交流社区 - BpSend.net asm基础语法 1. 环境配置 xp环境配置 1.拷贝masm615到指定目录 2.将masm615目录添加进环境变量 3.在cmd中输入ml&#xff0c;可以识别即配置成功 dosbox…

C/C++ 数据结构与算法【树和二叉树】 树和二叉树,二叉树先中后序遍历详细解析【日常学习,考研必备】带图+详细代码

一、树介绍 1&#xff09;树的定义 树 (Tree) 是n(n≥0) 个结点的有限集。 若n 0&#xff0c;称为空树; 若n > 0&#xff0c;则它满足如下两个条件: &#xff08;1&#xff09;有且仅有一个特定的称为(Root)的结点; &#xff08;2&#xff09;其余结点可分为m(m≥0)个…

MVC架构模式

分析AccountTransferServlet类都负责了什么&#xff1f; 数据接收核心的业务处理数据库表中数据的crud操作负责了页面的数据展示做了很多 在不使用MVC架构模式的前提下&#xff0c;完成银行账户转账的缺点&#xff1a; 代码的复用性太差。因为没有进行职能分工&#xff0c;没有…

打破视障壁垒,百度文心快码无障碍版本助力视障IT从业者就业无“碍”

有AI无碍 钟科&#xff1a;被黑暗卡住的开发梦 提起视障群体的就业&#xff0c;绝大部分人可能只能想到盲人按摩。但你知道吗&#xff1f;视障人士也能写代码。 钟科&#xff0c;一个曾经“被黑暗困住”的人&#xff0c;他的世界&#xff0c;因为一场突如其来的疾病&#xff0c…

【RAG实战】语言模型基础

语言模型赋予了计算机理解和生成人类语言的能力。它结合了统计学原理和深度神经网络技术&#xff0c;通过对大量的样本数据进行复杂的概率分布分析来学习语言结构的内在模式和相关性。具体地&#xff0c;语言模型可根据上下文中已出现的词序列&#xff0c;使用概率推断来预测接…

48页PPT|2024智慧仓储解决方案解读

本文概述了智慧物流仓储建设方案的行业洞察、业务蓝图及建设方案。首先&#xff0c;从政策层面分析了2012年至2020年间国家发布的促进仓储业、物流业转型升级的政策&#xff0c;这些政策强调了自动化、标准化、信息化水平的提升&#xff0c;以及智能化立体仓库的建设&#xff0…

Matlab环形柱状图

数据准备&#xff1a; 名称 数值 Aa 21 Bb 23 Cc 35 Dd 47 保存为Excel文件后&#xff1a; % Load data from Excel file filename data.xlsx; % Ensure the file is in the current folder or provide full path dataTable readtable(filena…

flask后端开发(3):html模板渲染

目录 渲染模板html模板获取路由参数 gitcode地址&#xff1a; https://gitcode.com/qq_43920838/flask_project.git 渲染模板 这样就能够通过html文件来渲染前端&#xff0c;而不是通过return了 html模板获取路由参数

15 break和continue

while True: content input("请输入你要喷的内容") print("发送给下路",content) #上述的程序如果没有外力干扰&#xff1a;程序会一直进行输入下去 #break:就能让当前这个循环立即进行停止 while True: content input("请输入…

Python9-作业2

记录python学习&#xff0c;直到学会基本的爬虫&#xff0c;使用python搭建接口自动化测试就算学会了&#xff0c;在进阶webui自动化&#xff0c;app自动化 python基础8-灵活运用顺序、选择、循环结构 作业2九九乘法表三种方式打印九九乘法表使用两个嵌套循环使用列表推导式和…