【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光纤反射内存卡”作为新一代存储技术的佼佼者,正逐步引领着数据存储领域的新风尚。本文将深入探讨这一创新产品的技术原理、性能优势、应用场景以及未来展望&…...
Linux链表操作全解析
Linux C语言链表深度解析与实战技巧 一、链表基础概念与内核链表优势1.1 为什么使用链表?1.2 Linux 内核链表与用户态链表的区别 二、内核链表结构与宏解析常用宏/函数 三、内核链表的优点四、用户态链表示例五、双向循环链表在内核中的实现优势5.1 插入效率5.2 安全…...
ESP32读取DHT11温湿度数据
芯片:ESP32 环境:Arduino 一、安装DHT11传感器库 红框的库,别安装错了 二、代码 注意,DATA口要连接在D15上 #include "DHT.h" // 包含DHT库#define DHTPIN 15 // 定义DHT11数据引脚连接到ESP32的GPIO15 #define D…...
如何理解 IP 数据报中的 TTL?
目录 前言理解 前言 面试灵魂一问:说说对 IP 数据报中 TTL 的理解?我们都知道,IP 数据报由首部和数据两部分组成,首部又分为两部分:固定部分和可变部分,共占 20 字节,而即将讨论的 TTL 就位于首…...
【Oracle】分区表
个人主页:Guiat 归属专栏:Oracle 文章目录 1. 分区表基础概述1.1 分区表的概念与优势1.2 分区类型概览1.3 分区表的工作原理 2. 范围分区 (RANGE Partitioning)2.1 基础范围分区2.1.1 按日期范围分区2.1.2 按数值范围分区 2.2 间隔分区 (INTERVAL Partit…...
vulnyx Blogger writeup
信息收集 arp-scan nmap 获取userFlag 上web看看 一个默认的页面,gobuster扫一下目录 可以看到扫出的目录中得到了一个有价值的目录/wordpress,说明目标所使用的cms是wordpress,访问http://192.168.43.213/wordpress/然后查看源码能看到 这…...
GO协程(Goroutine)问题总结
在使用Go语言来编写代码时,遇到的一些问题总结一下 [参考文档]:https://www.topgoer.com/%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B/goroutine.html 1. main()函数默认的Goroutine 场景再现: 今天在看到这个教程的时候,在自己的电…...
「全栈技术解析」推客小程序系统开发:从架构设计到裂变增长的完整解决方案
在移动互联网营销竞争白热化的当下,推客小程序系统凭借其裂变传播、精准营销等特性,成为企业抢占市场的利器。本文将深度解析推客小程序系统开发的核心技术与实现路径,助力开发者打造具有市场竞争力的营销工具。 一、系统核心功能架构&…...
6️⃣Go 语言中的哈希、加密与序列化:通往区块链世界的钥匙
Go 语言中的哈希、加密与序列化:通往区块链世界的钥匙 一、前言:离区块链还有多远? 区块链听起来可能遥不可及,似乎是只有密码学专家和资深工程师才能涉足的领域。但事实上,构建一个区块链的核心并不复杂,尤其当你已经掌握了一门系统编程语言,比如 Go。 要真正理解区…...
【免费数据】2005-2019年我国272个地级市的旅游竞争力多指标数据(33个指标)
旅游业是一个城市的重要产业构成。旅游竞争力是一个城市竞争力的重要构成部分。一个城市的旅游竞争力反映了其在旅游市场竞争中的比较优势。 今日我们分享的是2005-2019年我国272个地级市的旅游竞争力多指标数据!该数据集源自2025年4月发表于《地理学报》的论文成果…...
大数据驱动企业决策智能化的路径与实践
📝个人主页🌹:慌ZHANG-CSDN博客 🌹🌹期待您的关注 🌹🌹 一、引言:数据驱动的企业竞争力重构 在这个瞬息万变的商业时代,“快者胜”的竞争逻辑愈发明显。企业如何在复杂环…...
