当前位置: 首页 > news >正文

【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 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!")}
}

总结

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

相关文章:

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

文章目录 gin框架路由详解&#xff08;1&#xff09;go mod tidy&#xff08;2&#xff09;r : gin.Default()&#xff08;3&#xff09;r.GET()路由注册 &#xff08;4&#xff09;r.Run()路由匹配 总结 gin框架路由详解 先创建一个项目&#xff0c;编写一个简单的demo&#…...

GPIO+TIM(无PWM)实现呼吸灯功能

程序特点&#xff1a; 1、模块化&#xff0c;可快速移植&#xff0c;5分钟便可完成移植。 2、通过GPIO普通定时器&#xff0c;实现呼吸灯功能。 3、PWM周期为5ms&#xff0c;占空比调节时间为20ms&#xff0c;占空比为100等份&#xff0c;即呼吸灯从暗到亮需要20ms*1002s。 …...

贪心算法.

贪心算法是指只从当前角度出发,做出当前情景下最好的选择,在某种意义上来说是局部最优解,并不从全局的角度做决策.如果贪心策略选择不恰当,可能无法得到全局最优解. 贪心算法的基本流程如下: 1.分析问题,确定优化目标,对变量进行初始化 2.制定贪心策略:在制定贪心策略时需要…...

Linux系统和makefile详解

### Linux系统详解 Linux是一个开源且功能强大的操作系统内核&#xff0c;自1991年由林纳斯托瓦兹首次发布以来&#xff0c;它已经成为全球最流行的操作系统之一。Linux的核心特性包括开源、多用户多任务、高稳定性与安全性&#xff0c;以及良好的跨平台能力。 1. **开源**&a…...

GitLab 将停止为中国区用户提供服务,60天迁移期如何应对? | LeetTalk Daily

“LeetTalk Daily”&#xff0c;每日科技前沿&#xff0c;由LeetTools AI精心筛选&#xff0c;为您带来最新鲜、最具洞察力的科技新闻。 GitLab作为一个广受欢迎的开源代码托管平台&#xff0c;近期宣布将停止服务中国大陆、澳门和香港地区的用户提供服务。根据官方通知&#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 ​ }&#xff0c;“连续子列”被定义为{ N i ​ , N i1 ​ , …, N j ​ }&#xff0c;其中 1≤i≤j≤K。“最大子列和”则被定义为所有连续子列元素的和中最大者。例如给定序列{ -2, 1…...

深入浅出:AWT的基本组件及其应用

目录 前言 1. AWT简介 2. AWT基本组件 2.1 Button&#xff1a;按钮 2.2 Label&#xff1a;标签 ​编辑 2.3 TextField&#xff1a;文本框 2.4 Checkbox&#xff1a;复选框 2.5 Choice&#xff1a;下拉菜单 2.6 List&#xff1a;列表 综合案例 注意 3. AWT事件处理 …...

MySQL45讲 第三十六讲 为什么临时表可以重名?——阅读总结

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

WebRTC服务质量(11)- Pacer机制(03) IntervalBudget

WebRTC服务质量&#xff08;01&#xff09;- Qos概述 WebRTC服务质量&#xff08;02&#xff09;- RTP协议 WebRTC服务质量&#xff08;03&#xff09;- RTCP协议 WebRTC服务质量&#xff08;04&#xff09;- 重传机制&#xff08;01) RTX NACK概述 WebRTC服务质量&#xff08;…...

.NET常用的ORM框架及性能优劣分析总结

市面上有很多流行的 ORM&#xff08;对象关系映射&#xff09;框架可以用于 .NET 开发。本文主要针对以下几种常见的 ORM 框架&#xff0c;对其优劣进行分析及总结&#xff0c;希望能够帮助大家进行ORM框架的使用有所帮助。 1. Entity Framework (EF) 特点 • 官方支持&…...

Ubuntu网络配置(桥接模式, nat模式, host主机模式)

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

光通信复习

第一章 1.5 光纤通信系统的基本组成是怎么样的&#xff1f;试画出简图予以说明 光纤&#xff1a;主要负责光信号的传输光发送器&#xff1a;将用户端的电信号转化为光信号&#xff0c;入射到光纤内部光中继器&#xff1a;将光纤中发生衰减和畸变的光信号变成没有衰减和畸变的原…...

数字化转型中的投资决策:IT平台投资与业务应用投资的思考

在数字化转型的大潮中&#xff0c;企业常常面临一个核心问题&#xff1a;如何在繁杂的投资决策中精准地分配资源&#xff0c;特别是在IT平台投资和业务应用投资之间&#xff0c;如何合理划分责任与投入&#xff1f;在一些大型企业中&#xff0c;尤其是华为&#xff0c;针对不同…...

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中&#xff0c;相机的建模是 SLAM 系统的核心之一&#xff0c;因为它直接影响到如何处理和利用图像数据进行定位和地图构建。ORB-SLAM3 支持不同的相机模型&#xff0c;其中包括针孔模型和鱼眼模型&#xff08;K8 模型&#xff09;。下面分别介绍这两种模型。 相机…...

Python函数(十二):函数的创建和调用、参数传递、返回值

前言&#xff1a;在编程的世界里&#xff0c;函数是一种基本的构建块&#xff0c;它允许我们将代码封装成可重复使用的单元。在Python中&#xff0c;函数的使用尤为重要&#xff0c;因为它不仅有助于代码的模块化&#xff0c;还提高了代码的可读性和可维护性。本章节&#xff0…...

掌握Docker命令与Dockerfile实战技巧:快速构建高效容器化应用

1. 介绍 Docker 是现代开发和运维的必备工具&#xff0c;集成了容器技术的优势。本文将记录 Docker 的常用指令&#xff0c;并会随着使用经验的积累进行不定期更新。 2. 常用命令 2.1 启动容器&#xff08;前台交互模式&#xff09; docker run --privileged --volume /hom…...

Virtualbox硬盘扩容

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

10G光纤反射内存卡

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

华为云AI开发平台ModelArts

华为云ModelArts&#xff1a;重塑AI开发流程的“智能引擎”与“创新加速器”&#xff01; 在人工智能浪潮席卷全球的2025年&#xff0c;企业拥抱AI的意愿空前高涨&#xff0c;但技术门槛高、流程复杂、资源投入巨大的现实&#xff0c;却让许多创新构想止步于实验室。数据科学家…...

label-studio的使用教程(导入本地路径)

文章目录 1. 准备环境2. 脚本启动2.1 Windows2.2 Linux 3. 安装label-studio机器学习后端3.1 pip安装(推荐)3.2 GitHub仓库安装 4. 后端配置4.1 yolo环境4.2 引入后端模型4.3 修改脚本4.4 启动后端 5. 标注工程5.1 创建工程5.2 配置图片路径5.3 配置工程类型标签5.4 配置模型5.…...

Spark 之 入门讲解详细版(1)

1、简介 1.1 Spark简介 Spark是加州大学伯克利分校AMP实验室&#xff08;Algorithms, Machines, and People Lab&#xff09;开发通用内存并行计算框架。Spark在2013年6月进入Apache成为孵化项目&#xff0c;8个月后成为Apache顶级项目&#xff0c;速度之快足见过人之处&…...

VB.net复制Ntag213卡写入UID

本示例使用的发卡器&#xff1a;https://item.taobao.com/item.htm?ftt&id615391857885 一、读取旧Ntag卡的UID和数据 Private Sub Button15_Click(sender As Object, e As EventArgs) Handles Button15.Click轻松读卡技术支持:网站:Dim i, j As IntegerDim cardidhex, …...

【Go】3、Go语言进阶与依赖管理

前言 本系列文章参考自稀土掘金上的 【字节内部课】公开课&#xff0c;做自我学习总结整理。 Go语言并发编程 Go语言原生支持并发编程&#xff0c;它的核心机制是 Goroutine 协程、Channel 通道&#xff0c;并基于CSP&#xff08;Communicating Sequential Processes&#xff0…...

C# 类和继承(抽象类)

抽象类 抽象类是指设计为被继承的类。抽象类只能被用作其他类的基类。 不能创建抽象类的实例。抽象类使用abstract修饰符声明。 抽象类可以包含抽象成员或普通的非抽象成员。抽象类的成员可以是抽象成员和普通带 实现的成员的任意组合。抽象类自己可以派生自另一个抽象类。例…...

【Zephyr 系列 10】实战项目:打造一个蓝牙传感器终端 + 网关系统(完整架构与全栈实现)

🧠关键词:Zephyr、BLE、终端、网关、广播、连接、传感器、数据采集、低功耗、系统集成 📌目标读者:希望基于 Zephyr 构建 BLE 系统架构、实现终端与网关协作、具备产品交付能力的开发者 📊篇幅字数:约 5200 字 ✨ 项目总览 在物联网实际项目中,**“终端 + 网关”**是…...

安宝特案例丨Vuzix AR智能眼镜集成专业软件,助力卢森堡医院药房转型,赢得辉瑞创新奖

在Vuzix M400 AR智能眼镜的助力下&#xff0c;卢森堡罗伯特舒曼医院&#xff08;the Robert Schuman Hospitals, HRS&#xff09;凭借在无菌制剂生产流程中引入增强现实技术&#xff08;AR&#xff09;创新项目&#xff0c;荣获了2024年6月7日由卢森堡医院药剂师协会&#xff0…...

Chrome 浏览器前端与客户端双向通信实战

Chrome 前端&#xff08;即页面 JS / Web UI&#xff09;与客户端&#xff08;C 后端&#xff09;的交互机制&#xff0c;是 Chromium 架构中非常核心的一环。下面我将按常见场景&#xff0c;从通道、流程、技术栈几个角度做一套完整的分析&#xff0c;特别适合你这种在分析和改…...

二维FDTD算法仿真

二维FDTD算法仿真&#xff0c;并带完全匹配层&#xff0c;输入波形为高斯波、平面波 FDTD_二维/FDTD.zip , 6075 FDTD_二维/FDTD_31.m , 1029 FDTD_二维/FDTD_32.m , 2806 FDTD_二维/FDTD_33.m , 3782 FDTD_二维/FDTD_34.m , 4182 FDTD_二维/FDTD_35.m , 4793...