详细讲解go web框架之gin框架源码解析记录及思路流程和理解

Gin中间件绕过与PHP无参RCE:CTF混合环境下的Web安全攻防实战 Web安全的核心在于理解应用架构的薄弱环节。中间件作为请求处理管道的关键组件,负责认证、授权与过滤,其配置缺陷可能导致安全边界被突破。例如,路径解析不一致、大小写处理疏忽或URL解码差异,都可能形成绕过漏洞。一旦突破外围防护,攻击面便转向后端服务。在PHP环境中,无参数RCE技术展示了在严格输入限制下的代码执行能力,攻击者需巧妙利用内置函数(如getallheaders()、localeconv())和超全局变量构造利用链。这种从框架层到语言层的链式攻击,在微服务架构或遗留系统整合场景中尤为典型。本文以一道 阅读详情

开篇

开始

1. 从入门教程推出 Engine 对象

  • 从最开始的语句来入门教程:

    func main() {
    	// 创建一个默认的路由引擎
    	r := gin.Default()
    	// GET:请求方式;/hello:请求的路径
    	// 当客户端以GET方法请求/hello路径时,会执行后面的匿名函数
    	r.GET("/hello", func(c *gin.Context) {
    		// c.JSON:返回JSON格式的数据
    		c.JSON(200, gin.H{
    			"message": "Hello world!",
    		})
    	})
    	// 启动HTTP服务,默认在0.0.0.0:8080启动服务
    	r.Run()
    }
    
  • 我们点击进去 Run 函数的源码:

    //执行命令将路由器连接到http。服务器并开始侦听和服务HTTP请求。
    // http的快捷方式。ListenAndServe (addr,路由器)
    //注意:这个方法会无限期地阻塞调用goroutine,除非发生错误。
    func (engine *Engine) Run(addr ...string) (err error) {
    	defer func() { debugPrintError(err) }()
    
    	trustedCIDRs, err := engine.prepareTrustedCIDRs()
    	if err != nil {
    		return err
    	}
    	engine.trustedCIDRs = trustedCIDRs
    	address := resolveAddress(addr)
    	debugPrint("Listening and serving HTTP on %s\n", address)
    	err = http.ListenAndServe(address, engine)
    	return
    }
    
    • 其中最核心的语句就是 : err = http.ListenAndServe(address, engine)
      • 这就是调用的官方标准包, net/http。 可以去查看对应的用法:
        在这里插入图片描述

        • 它负责监听 tcp请求,并且交给 handler 参数去调用一个 Server函数处理接收到的链接。
        • 一般handler 为 nil, 那么默认有一个 DefaultServeMux 对象, 去调用 server函数处理请求。
      • 打开 http.ListenAndServe 源码,会发现,它获取的handler参数就是传递给 Server对象的。

        // ListenAndServe监听TCP网络地址addr,然后调用
        //使用handler处理传入连接的请求。
        //已接受的连接配置为启用TCP keep-alive。
        //
        //处理程序通常为nil,在这种情况下使用DefaultServeMux。
        //
        // ListenAndServe总是返回一个非nil错误。
        func ListenAndServe(addr string, handler Handler) error { 
        	server := &Server{Addr: addr, Handler: handler}   //源码中创建 server服务器对象,将handler传递进去
        	return server.ListenAndServe()
        }
        
      • Server函数: server对象的方法,接收每一个链接,并开启 go程,读取请求,调用handler回复请求。
        在这里插入图片描述

      • 默认的 DefaultServeMux

        // ServeMux是一个HTTP请求复用器。
        //它将每个传入请求的URL与一个已注册的列表进行匹配
        // 的模式,并调用处理程序的模式
        //最接近URL。
        //
        //模式名称固定,根路径,如"/favicon.ico",
        //或根子树,如"/images/"(注意后面的斜杠)。
        //较长的模式优先于较短的模式,因此
        //如果有两个处理程序注册"/images/"
        //和"/images/thumbnails/",后一个处理器将是
        //调用以"/images/thumbnails/"开头的路径
        //前将接收任何其他路径的请求
        / / /图片/子树。
        //
        //注意,因为以斜杠结尾的模式命名了根子树,
        //模式"/"匹配所有未被其他注册的路径
        //模式,而不仅仅是URL与Path == "/"。
        //
        //如果一个子树已经注册,并且接收到一个命名为
        //不带末尾斜杠的子树根,ServeMux重定向它
        //请求到子树根(添加末尾的斜杠)。这种行为可以
        //被一个单独的注册路径覆盖
        //末尾的斜杠。例如,注册“/images/”会导致ServeMux
        //重定向请求"/images"到"/images",除非"/images"有
        //已单独注册。
        //
        //模式可以选择以主机名开头,限制匹配为
        //该主机上的url。特定于主机的模式优先
        //通用模式,以便处理程序可以注册这两个模式
        // "/codesearch"和"codesearch.google.com/"不需要接管
        //请求“http://www.google.com/”。
        //
        // ServeMux还负责清理URL请求路径和主机
        //头,剥离端口号和重定向任何请求包含。或
        / / . .元素或重复的斜杠到等效的、更干净的URL。
        type ServeMux struct {
        	mu    sync.RWMutex
        	m     map[string]muxEntry
        	es    []muxEntry // slice of entries sorted from longest to shortest.
        	hosts bool       // whether any patterns contain hostnames
        }
        
        // NewServeMux allocates and returns a new ServeMux.
        func NewServeMux() *ServeMux { return new(ServeMux) }
        
        // DefaultServeMux is the default ServeMux used by Serve.
        var DefaultServeMux = &defaultServeMux
        
        var defaultServeMux ServeMux
        
      • 而对应的server对象在这

        type Server struct {
            Addr           string        // 监听的TCP地址,如果为空字符串会使用":http"
            Handler        Handler       // 调用的处理器,如为nil会调用http.DefaultServeMux
            ReadTimeout    time.Duration // 请求的读取操作在超时前的最大持续时间
            WriteTimeout   time.Duration // 回复的写入操作在超时前的最大持续时间
            MaxHeaderBytes int           // 请求的头域最大长度,如为0则用DefaultMaxHeaderBytes
            TLSConfig      *tls.Config   // 可选的TLS配置,用于ListenAndServeTLS方法
            // TLSNextProto(可选地)指定一个函数来在一个NPN型协议升级出现时接管TLS连接的所有权。
            // 映射的键为商谈的协议名;映射的值为函数,该函数的Handler参数应处理HTTP请求,
            // 并且初始化Handler.ServeHTTP的*Request参数的TLS和RemoteAddr字段(如果未设置)。
            // 连接在函数返回时会自动关闭。
            TLSNextProto map[string]func(*Server, *tls.Conn, Handler)
            // ConnState字段指定一个可选的回调函数,该函数会在一个与客户端的连接改变状态时被调用。
            // 参见ConnState类型和相关常数获取细节。
            ConnState func(net.Conn, ConnState)
            // ErrorLog指定一个可选的日志记录器,用于记录接收连接时的错误和处理器不正常的行为。
            // 如果本字段为nil,日志会通过log包的标准日志记录器写入os.Stderr。
            ErrorLog *log.Logger
            // 内含隐藏或非导出字段
        }
        
  • 可以看到, 我们的代码中: err = http.ListenAndServe(address, engine)

    • 也就是将 engine 传递进去, 作为 handler 参数,也就是不使用默认的 DefaultServeMux

    • engine 是 *Engine 类型。 也就是 我们 gin.Default() 出来的“引擎”(Default 出来的默认带有两个中间件)。 engin 既然可以作为 Handler 类型参数, 那么肯定实现了对应的接口:

      //处理程序响应HTTP请求。
      //
      // serverhttp应该向ResponseWriter写入应答头和数据
      //返回。返回请求完成的信号;它
      // 的ResponseWriter或从
      //请求。正文后或与之同时完成
      // ServeHTTP电话。
      //
      //根据HTTP客户端软件、HTTP协议版本和
      //客户端和Go服务器之间的任何中介,它可能不会
      //可以读取请求。身体写完后给
      // ResponseWriter。谨慎的处理程序应该读取请求。身体
      //首先,然后回复。
      //
      //除了读取主体外,处理程序不应该修改
      //请求提供。
      //
      //如果ServeHTTP崩溃,服务器(调用ServeHTTP的人)会假设
      //将panic的影响与主动请求隔离。
      //它恢复panic,记录堆栈跟踪到服务器错误日志,
      //关闭网络连接或者发送HTTP/2
      // RST_STREAM,取决于HTTP协议。中止处理程序,所以
      //客户端看到一个中断的响应,但服务器没有记录
      //一个错误,慌乱与值ErrAbortHandler。
      type Handler interface {
      	ServeHTTP(ResponseWriter, *Request)
      }
      
  • 总结:

    • 调用Run方法, 实际底层是使用 http包的方法, 将 Engine 类型对象作为 handler 参数传递进去(实现了Handler接口),构造 Server对象, 代替默认的 DefaultServeMux 处理请求链接,DefaultServeMux 实现了 相关的 match 等方法(去server.go 搜索ServeMux, 找到它实现的相关方法) 。

2. Engine 对象介绍及handleHTTPRequest接口实现与路由方法树树相关操作

  • 认清 Engine 对象 及giin框架。
    • 源码上方的介绍:Engine 是框架的实例,它包含muxer,中间件和配置设置。
      • 可以看到是非常重要的! 同时我们也知道了 gin 框架的主要作用, 就是 充当了 muxer 代替 net/http 包里 启动服务器对象的默认 muxer, 同时还提供了 中间件,配置设置等功能
  • 我们查看源码,找到Egine 对象: 比较长,注释很清晰,每个功能的作用。
    • 参考文章: https://zhuanlan.zhihu.com/p/102303084, Engine 相关常用的方法,在这里都有介绍。一定进去查看下!

      • 理解了其实还可以使用 goland编辑器, 鼠标停在gin,Default()。Defalut上。 然后出现返回类型, *Engine, 点击一下,就会出现相关具有的方法 和 内容。(缺少一个maxParams参数,老版本这个参数是在路由树的里定义的)
      //Engine是框架的实例,它包含muxer,中间件和配置设置。
      //创建一个Engine实例,使用New()或Default()
      type Engine struct {
          // 路由组,在实际开发过程中我们通常会使用路由组来组织和管理一些列的路由. 比如: /apis/,/v1/等分组路由
          RouterGroup
          // 开启自动重定向。如果当前路由没有匹配到,但是存在不带/开头的handler就会重定向. 比如: 用户输入/foo/但是存在一个/foo 就会自动重定向到该handler,并且会向客户端返回301或者307状态码(区别在于GET方法和其他方法)
          RedirectTrailingSlash bool
          // 如果开启该参数,没有handler注册时,路由会尝试自己去修复当前的请求地址.
          // 修复流程:
          // 1.首位多余元素会被删除(../ or //); 2.然后路由会对新的路径进行不区分大小写的查找;3.如果能正常找到对应的handler,路由就会重定向到正确的handler上并返回301或者307.(比如: 用户访问/FOO 和 /..//Foo可能会被重定向到/foo这个路由上)
          RedirectFixedPath bool
          // 如果开启该参数,当当前请求不能被路由时,路由会自己去检查其他方法是否被允许.在这种情况下会响应"Method Not Allowed",并返回状态码405; 如果没有其他方法被允许,将会委托给NotFound的handler
          HandleMethodNotAllowed bool
          // 是否转发客户端ip
          ForwardedByClientIP    bool
          // 如果开启将会在请求中增加一个以"X-AppEngine..."开头的header
          AppEngine bool
          // 如果开启将会使用url.RawPath去查找参数(默认:false)
          UseRawPath bool
          // 如果开启,请求路径将不会被转义. 如果UseRawPath为false,该参数实际上就为true(因为使用的是url.Path)
          UnescapePathValues bool
          // maxMemory参数的值(http.Request的ParseMultipartForm调用时的参数)
          MaxMultipartMemory int64
          // 是否删除额外的反斜线(开始时可解析有额外斜线的请求)
          RemoveExtraSlash bool
          // 分隔符(render.Delims表示使用HTML渲染的一组左右分隔符,具体可见html/template库)
          delims           render.Delims
          // 设置在Context.SecureJSON中国的json前缀
          secureJsonPrefix string
          // 返回一个HTMLRender接口(用于渲染HTMLProduction和HTMLDebug两个结构体类型的模板)
          HTMLRender       render.HTMLRender
          // html/template包中的FuncMap map[string]interface{} ,用来定义从名称到函数的映射
          FuncMap          template.FuncMap
          // 以下是gin框架内部定义的一些属性
          // HandlersChain 是一个HandlerFunc 的数组(HandlerFunc其实就是一个Context的指针,Context会在下一节讲解)
          allNoRoute       HandlersChain
          allNoMethod      HandlersChain
          noRoute          HandlersChain       // 上述连接中有介绍
          noMethod         HandlersChain
          // 这里定义了一个可以临时存取对象的集合(sync.Pool是线程安全的,主要用来缓存为使用的item以减少GC压力,使得创建高效且线程安全的空闲队列)
          pool             sync.Pool
          // methodTrees是methodTree的切片(methodTree是一个包含请求方法和node指针的结构体,node是一个管理path的节点树)
          trees            methodTrees
      }
      
    • 下方还有一句:var _ IRouter = &Engine{} 通过匿名变量定义 某个接口类型的值,这样的写法,作用是,确保 Engine{} 实现了 IRouter 接口。 一个技巧。

    • 找到对应实现接口的方法: ServeHTTP:

      // ServeHTTP conforms to the http.Handler interface.
      func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
      	// SYNC Pool对象池的概念: 减少内存申请,并 类型断言,转换为 Context类型指针。
      	c := engine.pool.Get().(*Context)
      	// 拿到对象后,下面三步才进行对象的初始化。
      	c.writermem.reset(w)
      	c.Request = req
      	//因为是结构体并且sync.pool机制不会主动重置`Context`,所以手动重置`Context`
      	c.reset()
      	// 关键一步,调用handleHTTPRequest函数,处理请求。
      	engine.handleHTTPRequest(c)
      
      	engine.pool.Put(c)
      }
      
    • 关键的一步就是处理http请求的函数: handleHTTPRequest ,传递进去初始化好的上下文。

    • 上源码:

      func (engine *Engine) handleHTTPRequest(c *Context) {
      	// 1. 获取相关数据 和 设置
      	httpMethod := c.Request.Method
      	rPath := c.Request.URL.Path
      	unescape := false
      	if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 {
      		rPath = c.Request.URL.RawPath
      		unescape = engine.UnescapePathValues
      	}
      
      	if engine.RemoveExtraSlash {
      		rPath = cleanPath(rPath)
      	}
      
      	// Find root of the tree for the given HTTP method
      	// 找到路由树
      	// 执行请求对应的函数,写入Response
      	t := engine.trees
      	for i, tl := 0, len(t); i < tl; i++ {
      		if t[i].method != httpMethod {
      			continue
      		}
      		root := t[i].root
      		// Find route in tree
      		value := root.getValue(rPath, c.params, unescape)
      		if value.params != nil {
      			c.Params = *value.params
      		}
      		if value.handlers != nil {
      			c.handlers = value.handlers
      			c.fullPath = value.fullPath
      			c.Next()
      			c.writermem.WriteHeaderNow()
      			return
      		}
      		if httpMethod != "CONNECT" && rPath != "/" {
      			if value.tsr && engine.RedirectTrailingSlash {
      				redirectTrailingSlash(c)
      				return
      			}
      			if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) {
      				return
      			}
      		}
      		break
      	}
      
      	if engine.HandleMethodNotAllowed {
      		for _, tree := range engine.trees {
      			if tree.method == httpMethod {
      				continue
      			}
      			if value := tree.root.getValue(rPath, nil, unescape); value.handlers != nil {
      				c.handlers = engine.allNoMethod
      				serveError(c, http.StatusMethodNotAllowed, default405Body)
      				return
      			}
      		}
      	}
      	c.handlers = engine.allNoRoute
      	serveError(c, http.StatusNotFound, default404Body)
      }
      
    • 源码解析:

      • (1) 首先获得 请求方法等数据,信息,进行相关操作。
      • (2)获取 gin.trees, 路由方法树。
        • 这里参考之前写的路由树的,前缀树Trie, Radix树相关的内容和实现。:go语言简单实现 前缀树(Trie)与了解基数树(Radix-Tree)

        • 对应的是 Engine里面的 trees methodTrees 类型。 而methodTrees 类型是这样定义的: type methodTrees []methodTree

        • methodTree 是这样定义的:

          //  记录当前的路由字符 和 对应的 radix或前缀树的根节点!
          type methodTree struct {
          	method string
          	root   *node
          }
          
        • 对应的节点类型 node 为: 这和我们自己实现的前缀树基本一致,可以看到多了一些属性, 并且还具有相关 addRoute等方法。

          type node struct {
          	path      string
          	indices   string
          	wildChild bool
          	nType     nodeType
          	priority  uint32
          	children  []*node // child nodes, at most 1 :param style node at the end of the array
          	handlers  HandlersChain
          	fullPath  string
          }
          
        • 我们顺藤摸瓜,前面从 methodTree 找到了 请求方法对应的树结构node节点类型。那么各个字段的含义呢?

          • path: 节点路经字符串“当前节点保存的 路径字符”
          • indices(目录,索引):和children字段是对应的。保存的是 children 节点的 path字段的 首字母。在进行查找的时候,就可以通过当前字段来判断,该树的子节点是否满足后序的匹配规则。
          • children :保存儿子节点
          • handlers HandlersChain: 处理函数的链条。 这里寻找对应的处理函数,以及执行流程! 还可以用来判断当前节点是否是正确的路由,当前节点 handlers字段为 nil 说明没有注册处理函数, 就是一个正确的 路由!!(我的想法)
          • priority: uint32 : 优先级,表示子节点,子子节点注册的handler数量。
          • nType nodeType: 表示节点类型。其中的类型:
            type nodeType uint8
            
            const (
            	static nodeType = iota // default   默认值,静态的节点。
            	root			表示是根节点
            	param		参数节点,就例如 router.GET("/user/:name", func(c *gin.Context), name := c.Param("name") 获取捕获的姓名
            	catchAll     表示当前节点有 匹配符 * ,匹配
            )
            
          • wildChild bool:节点是否是参数节点
          • fullPath string:完整路径
        • 下面实战讲解,进行测试,以我的一个抽奖项目为例,路由组的代码是这样的:

          • 当然了,每个路由组里面还有更细小的路由。
          	// 指定路由,加载路由
          	// 1. 网站首页
          	v1 := app.Group("/")
          	addIndexRouters(v1, &Index)
          
          	// 2. admin 后台管理路由
          	v2 := app.Group("/admin")
          	// 启用中间件: BasicAuth()(验证)中间件
          	v2.Use(gin.BasicAuth(gin.Accounts{
          		"admin": "123456", // 添加一个管理员用户
          	}))
          	addAdminRouters(v2, &Admin)
          
          	// 3. 商品管理页
          	v3 := v2.Group("/gift")
          	addAdminGiftRouters(v3, &AdminGift)
          
          	// 4. 优惠券管理页
          	v4 := v2.Group("/code")
          	addAdminCodeRouters(v4, &AdminCode)
          
          	// 5. 中奖结果管理
          	v5 := v2.Group("/result")
          	addAdminResultRouters(v5, &AdminResult)
          
          	// 6. 用户管理
          	v6 := v2.Group("/user")
          	addAdminUserRouters(v6, &AdminUser)
          
          	// 7. ip 黑名单管理
          	v7 := v2.Group("/blackip")
          	addAdminBlackipRouters(v7, &AdminBlackip)
          
        • 可以看到,engin.tress 的结构是这样的:我目前有 GET,GEAD,POST三种方法。
          在这里插入图片描述

        • 那么对应的内部 node节点如何?打开POST方法对应的路由树来看里面路由比较少,大致就是这种:
          在这里插入图片描述

        • 再来分析一波小的GET方法下: 可以看到对应的 “l” 的Priority 是3,说明下面有三个处理函数。三个注册的handler。
          在这里插入图片描述

        • 以 /login路由为例子,我怎么区分是否 是正确路由呢? 当节点的 handlers 为nil,说明不存在该路由!
          在这里插入图片描述

        • 总结:路由方法树

          • 1 前缀树,或者说是Radix 基数树数据及结构
          • 2 通过 handlers 判断是否是 尾值(是否是正确路由)
          • 3 注意node节点的结构, indexes 表示子节点的 path 首字母,用于搜索。还有节点类型等一些其他的特性。 实现具体路由功能,要比 基本的数据结构radix复杂一些。
      • (3) 遍历找到对应方法,gin中是采用slice实现的,并没有使用map结构!
        • 首先,这里 t 是个 slice结构,那么for遍历它寻找对应方法的操作就是 线性的!为啥要用线性的浪费时间呢? 而不使用 map 结构?
          • 注意代码中比较的是 methodTree.root 字段。 它代表是的表示一个请求的方法,例如 GET,POST等的字符串。
          • 在http1.0时。只有 GET,POST,OPTIONS 三种
          • 1.1时有了九种,PUT,DELETE等等。
          • 但是一个网站常用的请求,也就四五种,不会太多。所以这里采用线性遍历寻找,也是可以接受的! 数据规模不大,且可以节省内存的开销。
          • 另外,t := engine.trees。如何验证这个 t 是在何时进行初始化的? 内存如何分配呢? 这就又引出来一个问题了。
            • t(engine.trees) 肯定是Engine初始化的时候,进行申请的。

            • Engine 初始化有两种方法, gin.Default() 和 gin.New()

            • gin.Default 内部其实是使用了 gin.New() 并且增加了 两个中间件,日志和恢复。

            • 来看下Default 源码:

              // Default returns an Engine instance with the Logger and Recovery middleware already attached.
              func Default() *Engine {
              	debugPrintWARNINGDefault()
              	engine := New()
              	engine.Use(Logger(), Recovery())
              	return engine
              }
              
            • 查看对应的New()源码,就能看到 对 Engine对象的各种初始化工作!其中有很关键的一句:trees: make(methodTrees, 0, 9), 证明了 路由方法树,不是运行时动态申请内存的,而是初始化的时候,就申请了9个单位的空间,初始化长度为 0!

        • 这里遍历 t的过程,有个小技巧, 先进行计算出来 len(t), 后续比较直接使用变量就可以了, 避免了每次动态重新计算 len(t)。 当然,如果遍历的过程中需要 更改这个t, 就不能这样了, len() 每次执行都会动态的重新获取 len() 长度。
      • (4) 这里讲了三点,还没分析完这个函数逻辑功能。再第四:路由匹配规则中会继续介绍。
  • 那么主要就是看

3. 注册路由的过程

  • 同样参照开篇中图片的那篇文章,调理很清晰! https://zhuanlan.zhihu.com/p/102303084,可以先看下路由大致的内容。
  • 注册路由的主要逻辑有俩函数 addRoute函数 和 insertChild 方法
  • 从最开始的注册路由函数看起:
    	// 创建路由组
    	v1 := app.Group("/")
    	// 首页
    	rg.GET("/", indexCol.Get)
    	// 加不加斜线都行
    	rg.GET("/login", indexCol.GetLogin)
    	rg.GET("logout", indexCol.GetLogout)
    
    	简单的还可以直接创建添加路由
    	app := gin.Default
    	app.bubble.GET("/todo", fun HandlerFunc)...
    
    • 路由和路由组添加调用的都是同一个 GET 方法。 还有POST,DELETE等方法,我们只看 GET,POST,都是一样的:

    • 翻译中说了 : POST是路由器的一种快捷方式, 内部是调用 Handle(“POST”, path, handle) 来进行处理。

      // POST is a shortcut for router.Handle("POST", path, handle).
      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, handle).
      func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
      	return group.handle(http.MethodGet, relativePath, handlers)
      }
      
    • 那么这是咋回事儿? 怎么突然从 Engin 的Default 对象就到了 (group *RouterGroup) 对象了呢? GET, POST, 还有创建路由组Group,都是 (group *RouterGroup)提供的! 内部还调用它的 handle 进行处理。
      我们再次返回前文观察到: Engine 对象的结构中,第一行就出现了: RouterGroup 这是使用了 Embedding: 内嵌 的方式扩展已有类型。

      • 一共有三种,前面的博客中已经讲解过了扩展已有类型:别名,内嵌,组合三种方式。

      • 所以我们创建路由和路由组的时候,使用的是 RouterGroup提供的方法!

      • 那么 RouterGroup 对象的简答初始化,其实也是gin.New() 中了

        RouterGroup: RouterGroup{
                    Handlers: nil,
                    basePath: "/",
                    root:     true,
                },
        
    • 继续说回处理路由,POST交给 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()
      }
      
      • POST 请求传递的多个 handlers …HandlerFunc HandlerFunc类型函数, 被handle 函数的 HandlersChain 类型接收。

      • HandlersChain 实际上就是: type HandlersChain []HandlerFunc,还有HandlerFunc。

        // HandlerFunc将gin中间件使用的处理器定义为返回值
        type HandlerFunc func(*Context)
        
        // HandlersChain定义一个HandlerFunc数组。
        type HandlersChain []HandlerFunc
        
      • 剩下的两个参数: 一个是 http 请求方法, 一个是 相对路径。

      • 分析handle 函数都干了什么:

        • 1 calculateAbsolutePath通过名字可以看出来,这是计算绝对的路径地址,通过相对路径。
        • 2 combineHandlers 合并group 自带的和 开发者注册的函数(handler)
          func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
          	// 将开发者注册的 handlers 和 group 系统的一些处理handler 进行拼接。
          	finalSize := len(group.Handlers) + len(handlers)
          	// 超出指定长度报错
          	if finalSize >= int(abortIndex) {
          		panic("too many handlers")
          	}
          	// 合并操作,主要就是 copy 和 make 函数的使用
          	mergedHandlers := make(HandlersChain, finalSize)
          	copy(mergedHandlers, group.Handlers)
          	//  这一点可以,切片的切片 去复制。是个不错的技巧。
          	copy(mergedHandlers[len(group.Handlers):], handlers)
          	return mergedHandlers
          }
          
          • 注意最后一步复制的顺序和技巧
          • 另外, group 对象的 handlers是从哪里来的呢? 有个对外的方法:Use() 实际上就是添加中间件的函数。
            func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
            	group.Handlers = append(group.Handlers, middleware...)
            	return group.returnObj()
            }
            
      • 3 下一步拼接好的 处理handlers, 加上http 方法名字, 绝对路径传递给 addRoute 方法。注意这个addRoute方法,是调用的 engine 对象的。原因就是,最开始讲 engine对象的时候,其结构体中包含 trees, 路由方法树。 用于添加路由和路由方法。

        func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
        	// 一系列断言操作
        	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")
        
        	debugPrintRoute(method, path, handlers)
        	// 通过get方法,内部也是线性for循环遍历,找到对应方法的 方法树
        	root := engine.trees.get(method)
        	// 不存在就添加新的方法树根节点
        	if root == nil {
        		root = new(node)
        		// 默认其实 root 的字符必须是 "/"
        		root.fullPath = "/"
        		engine.trees = append(engine.trees, methodTree{method: method, root: root})
        	}
        	// 调用节点的  addRoute 方法,往对应方法树内插入
        	root.addRoute(path, handlers)
        
        	// Update maxParams
        	// 更新 maxParams 参数
        	if paramsCount := countParams(path); paramsCount > engine.maxParams {
        		engine.maxParams = paramsCount
        	}
        }
        
        • 其中到了最核心的步骤,找到对应的方法树后(root,方法树的根节点,node类型)下面的插入操作就交给了 node 类中的 addRoute方法实现了。 传递参数:path: 绝对路径地址; handlers: 组合的注册handle。

        • 到了这里,其实就是底层实现的Radix树, 传进去一个字符串(完整路由路径),添加到Radix树中。 源码函数比较长, 大致过程理解,但是细节还理解不是很到位~ (这一点没有理解明白)

        • 相关的函数功能都有在文档中解释,比较难得,重要的放到这个源码后面再去拿出来分析。

          // addRoute将一个具有给定句柄的节点添加到路径。
          // 不是concurrency-safe (不是并发安全的)
          func (n *node) addRoute(path string, handlers HandlersChain) {
          	// 补充fullpath属性
          	fullPath := path
          	// priority++ 优先级加1 
          	n.priority++
          
          	// Empty tree
          	// 如果是空的树,或还没有子节点:初始化是一个 "/",node类型, 调用insertChild插入
          	if len(n.path) == 0 && len(n.children) == 0 {
          		// "/"  中插入path
          		n.insertChild(path, fullPath, handlers)
          		// 因为是空树这里将调用的节点(“/”)标记为 根节点类型。
          		n.nType = root
          		return
          	}
          
          	parentFullPathIndex := 0
          
          walk:
          	for {
          		//查找最长的公共前缀。
          		//这也暗示了公共前缀不包含':'或'*'
          		// 因为现有的键不能包含这些字符。
          		// 这个 函数非常简单,就是 for循环,找到两个path的公共最长前缀,返回其长度。 
          		// 比如 /index , /index/a  公共长度就是 /index 
          		i := longestCommonPrefix(path, n.path)
          
          		// Split edge: 分裂,找到每一部分
          		// 分类的是当前节点。 比方当前是 "login" 新增是 “logout”
          		// 那么当前节点按照最长前缀进行分裂 log,  增加 in, out 两个子节点。
          		if i < len(n.path) {
          			// 先将当前 node 自己分开,后半截作为子节点
          			child := node{
          				path:      n.path[i:],
          				wildChild: n.wildChild,
          				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
          			// 改变 当前节点为前半段
          			// 保存子节点的 path 首字母
          			n.indices = bytesconv.BytesToString([]byte{n.path[i]})
          			n.path = path[:i]
          			n.handlers = nil
          			n.wildChild = false
          			n.fullPath = fullPath[:parentFullPathIndex+i]
          		}
          
          		// Make new node a child of this node
          		// 和上面的逻辑是相似的,比较 len(path)
          		// i < len(path) 说明需要进行拆分
          		if i < len(path) {
          			path = path[i:]
          			c := path[0]
          
          			// '/' after param
          			// 处理参数后面有斜杠的情况
          			if n.nType == param && c == '/' && len(n.children) == 1 {
          				parentFullPathIndex += len(n.path)
          				n = n.children[0]
          				n.priority++
          				continue walk
          			}
          
          			// Check if a child with the next path byte exists
          			// 进行检测分配, 遍历indices,看看当前节点的子节点那个满足开头匹配。 
          			// 满足匹配后,再跳到子节点,n = n.children[i], 再次进行相关操作。 分裂或者创建。
          			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
          				}
          			}
          
          			// Otherwise insert it
          			// 子节点找到不的话,就尝试插入进去。
          			if c != ':' && c != '*' && n.nType != catchAll {
          				// []byte for proper unicode char conversion, see #65	
          				// 拼接第一个字符到 indices
          				n.indices += bytesconv.BytesToString([]byte{c})
          				child := &node{
          					fullPath: fullPath,
          				}
          				// 追加子节点,做了一个append操作内部
          				n.addChild(child)
          				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 wildcard
          				// 插入通配符节点时,需要检查是否与现有通配符冲突
          				n = n.children[len(n.children)-1]
          				n.priority++
          
          				// Check if the wildcard matches
          				if len(path) >= len(n.path) && n.path == path[:len(n.path)] &&
          					// Adding a child to a catchAll is not possible
          					n.nType != catchAll &&
          					// Check for longer wildcard, e.g. :name and :names
          					(len(n.path) >= len(path) || path[len(n.path)] == '/') {
          					continue walk
          				}
          
          				// Wildcard conflict
          				pathSeg := path
          				if n.nType != catchAll {
          					pathSeg = strings.SplitN(pathSeg, "/", 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 +
          					"'")
          			}
          
          			n.insertChild(path, fullPath, handlers)
          			return
          		}
          
          		// Otherwise add handle to current node
          		// 这个节点已经注册过了。
          		if n.handlers != nil {
          			panic("handlers are already registered for path '" + fullPath + "'")
          		}
          		n.handlers = handlers
          		n.fullPath = fullPath
          		return
          	}
          }
          
        • 核心操作就是:

          • (我觉得)主要的就是进行树的一个 分裂,调整,查找节点, 插入的操作由insertChild 函数完成,并对相应的通配符节点进行处理
          • 1 存在寻找位置插入
            • 不断的判断,通过indices 字段查找,然后或者分裂当前 path。重新调整 radix树。
          • 2 不存在先创建
          • 3 各种判断:比如节点存在,就不用创建
          • 4 对不同类型的节点进行特殊处理,比如param类型。
          • 5 相应的进行各种节点的信息初始化 与 更新
        • 注意:里面有个核心的插入操作: insertChild 函数。

          func (n *node) insertChild(path string, fullPath string, handlers HandlersChain) {
          	for {
          		// Find prefix until first wildcard
          		// 找到第一个通配符参数,一直查找前缀
          		// 搜索通配符段并检查名称中是否有无效字符。
          		// 如果没有找到通配符,则返回-1作为索引。
          		wildcard, i, valid := findWildcard(path)
          		if i < 0 { // No wildcard found
          			break
          		}
          
          		// The wildcard name must not contain ':' and '*'
          		// 通配符名称不能包含':'和'*'
          		if !valid {
          			panic("only one wildcard per path segment is allowed, has: '" +
          				wildcard + "' in path '" + fullPath + "'")
          		}
          
          		// check if the wildcard has a name
          		// 通配符是否有名字,小于2长度没有名字, :name
          		if len(wildcard) < 2 {
          			panic("wildcards must be named with a non-empty name in path '" + fullPath + "'")
          		}
          
          		if wildcard[0] == ':' { // param
          			// 对正常通配符进行相关处理
          			if i > 0 {
          				// Insert prefix before the current wildcard
          				n.path = path[:i]
          				path = path[i:]
          			}
          
          			child := &node{
          				nType:    param,
          				path:     wildcard,
          				fullPath: fullPath,
          			}
          			n.addChild(child)
          			n.wildChild = true
          			n = child
          			n.priority++
          
          			// if the path doesn't end with the wildcard, then there
          			// will be another non-wildcard subpath starting with '/'
          			//如果路径没有以通配符结束,那么就在那里
          			//将是另一个以'/'开头的非通配符子路径
          			if len(wildcard) < len(path) {
          				path = path[len(wildcard):]
          
          				child := &node{
          					priority: 1,
          					fullPath: fullPath,
          				}
          				n.addChild(child)
          				n = child
          				continue
          			}
          
          			// Otherwise we're done. Insert the handle in the new leaf	
          			// 到这里就完成了,插入节点完成
          			n.handlers = handlers
          			return
          		}
          
          		// catchAll
          		if 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] == '/' {
          			panic("catch-all conflicts with existing handle for the path segment root in path '" + fullPath + "'")
          		}
          
          		// 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 path
          		child := &node{
          			wildChild: true,
          			nType:     catchAll,
          			fullPath:  fullPath,
          		}
          
          		n.addChild(child)
          		n.indices = string('/')
          		n = child
          		n.priority++
          
          		// second node: node holding the variable
          		child = &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 handle
          	// 如果没有通配符,只需要插入路径和句柄
          	n.path = path
          	n.handlers = handlers
          	n.fullPath = fullPath
          }
          
          • 最主要的操作就是对通配符的处理。完成插入操作。
        • 1

      • 1

4. 路由匹配

  • 之前 2 一步步推理讲到 Engine 实现 ServerHTTP接口,代替 http包下的默认 muxer。 context 是在这里做的。然后讲解了核心路由匹配处理的函数 handleHTTPRequest(c) 讲了部分内容,现在继续讲解,理解有限很多地方还很简陋,理不顺,后续再来补充吧:

  • 其中的 getvalue 函数:getValue(path string, params *Params, unescape bool),通过传递的相关参数,从路由树查找,返回了一个: nodeValue 类型的对象,而不是节点对象。

    type nodeValue struct {
        handlers HandlersChain  // 注册的路由
        params   *Params  // 参数
        tsr      bool
        fullPath string
    }
    nodeValue holds return values of (*Node).getValue method
    
  • 后面的一段主要执行:

    if value.handlers != nil {
    			c.handlers = value.handlers
    			c.fullPath = value.fullPath
    			c.Next() // next函数,继续执行
    			c.writermem.WriteHeaderNow()
    			return
    		}
    

5. 中间件

  • 其实根据上面的分析,中间件的设计已经很清晰了,比较巧妙的设计。
  • gin.Default() 内部默认开启两个中间件:日志 和 恢复
    • 启用中间件的函数时 Use(), 它来自Engin 对象内部的 RouterGroup对象。

    • 具体干了啥呢?打开源码看一下

      // Use attaches a global middleware to the router. ie. the middleware attached though Use() will be
      // included in the handlers chain for every single request. Even 404, 405, static files...
      // For example, this is the right place for a logger or error management middleware.
      //将全局中间件附加到路由器ie。通过Use()附加的中间件将是
      //每个请求都包含在处理程序链中。甚至404,405,静态文件…
      //例如,这是一个日志或错误管理中间件的正确位置。
      func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
      	engine.RouterGroup.Use(middleware...)
      	// 整合 engin.noRout, engine.noMethod ,他俩其实就是 一个注册的 handler的slice
      	engine.rebuild404Handlers()
      	engine.rebuild405Handlers()
      	return engine
      }
      
    • 最终调用的 路有对象的 Use方法:意图很清晰,加入到 路由对象的 Handlers里去。 当调用路由的时候,有个合并操作! 将这些方法合并到对应子路由中去执行。

      // Use adds middleware to the group, see example code in GitHub.
      func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
      	group.Handlers = append(group.Handlers, middleware...)
      	return group.returnObj()
      }
      
    • 4.中介绍了 getValue,并将相应的数据,赋值给了 context对象。然后调用了一个 Next函数。 next主要做了什么?

      • next函数:按照 Handlers HandlersChain 注册函数链,进行依次调用执行。 顺序执行。

        // Next should be used only inside middleware.
        // It executes the pending handlers in the chain inside the calling handler.
        // See example in GitHub.
        // 依次取出 注册的函数,包括视图逻辑处理函数 以及注册的中间件函数。
        // 顺序执行
        func (c *Context) Next() {
        	c.index++
        	for c.index < int8(len(c.handlers)) {
        		c.handlers[c.index](c)
        		c.index++
        	}
        }
        
        • 小技巧: 如何实现非线性的调用呢?
          • 注意到 Next源码里有个开始的 ++ 操作。可以再某个处理逻辑handle 里调用 c.Next()函数! 此时会依次向下执行完剩余的 handle,后才接着处理本次请求。当然了这个 调用next函数,可以一直嵌套, 后面执行的 handle 里面还可以 调用c.next(Context对象)。改变执行顺序!
      • 讲了Next函数后, 必须要提一下Abort() 函数。代码实现超级简单,阅读过前面这里就很容易懂了! 就是更改 c.index = abortIndex。 index 是遍历 Handlers HandlersChain 的索引

        • abortIndex: 是一个常量: const abortIndex int8 = math.MaxInt8 / 2 终止后序的handle的执行。
        // Abort prevents pending handlers from being called. Note that this will not stop the current handler.
        // Let's say you have an authorization middleware that validates that the current request is authorized.
        // If the authorization fails (ex: the password does not match), call Abort to ensure the remaining handlers
        // for this request are not called.
        //中止处理程序被调用。注意,这不会停止当前的处理程序。
        //假设你有一个授权中间件来验证当前请求是否被授权。
        //如果授权失败(例如:密码不匹配),调用Abort以确保剩余的处理程序
        //这个请求没有被调用。
        func (c *Context) Abort() {
        
        c.index = abortIndex
        
        }
        func (c *Context) Abort() {
        	c.index = abortIndex
        }
        
        
      • 再来讲解 c.Set: 向接口中添加相应的数据,因为Context对象是贯穿整个流程的,所以后面执行的 handle 可以调用当前设置的这个值,为接口类型。

        // Set is used to store a new key/value pair exclusively for this context.
        // It also lazy initializes  c.Keys if it was not used previously.
        // Set用于为这个上下文专门存储一个新的键/值对。
        //如果c.Keys以前没有被使用过,它也会延迟初始化。
        func (c *Context) Set(key string, value interface{}) {
        	// 还是线程安全的添加,加了读写锁的。
        	c.mu.Lock()
        	if c.Keys == nil {
        		c.Keys = make(map[string]interface{})
        	}
        
        	c.Keys[key] = value
        	c.mu.Unlock()
        }
        
      • 除此之外还有一个 c.Get 可以获取相应的,就不再分析了。

6. 关于context对象 和 bind

  • 直接查看大佬文章吧~~~ 我还是菜鸡,这篇文章已经瞎写的有点长了,电脑太卡了。
  • https://zhuanlan.zhihu.com/p/102438603
  • 其实上面的 中间件后面已经讲到了一些context对象的内容了。
Gin框架日志控制实战:从基础输出到生产级结构化日志方案 在Web服务开发中,日志是系统可观测性的基石,它记录了请求处理、错误追踪和性能分析的关键信息。其核心原理是通过中间件拦截HTTP请求与响应,将结构化数据写入指定输出目标。良好的日志实践能极大提升运维效率和故障排查能力,尤其在微服务和云原生场景下至关重要。常见的应用场景包括请求链路追踪、性能监控、安全审计和业务分析。本文聚焦于Go语言Gin框架的日志控制,针对生产环境中常见的日志管理挑战,如海量日志导致的磁盘撑爆、非结构化日志难以解析等问题,深入探讨了如何实现日志的持久化、结构化输出与生命周期管理。通过集成Z 阅读详情

相关推荐

从零构建Gin可观测性:zap日志与otelgin链路追踪实战

微服务架构下,一次请求跨越多个节点,排查故障往往需要反复切换日志、监控和调用链工具,效率极低。可观测性作为现代后端系统的核心能力,通过日志、指标与链路追踪三大支柱,将请求全链路透明化。其中,日志提供业务细节,链路追踪还原调用拓扑与耗时,二者通过traceID关联,即可实现从入口到数据层的端到端定位。在Go生态中,gin作为主流Web框架,可借助zap实现高性能结构化日志,通过otelgin自动注入HTTP链路上下文,并利用ORM钩子将SQL查询纳入同一追踪体系。这套方案不仅适用于微服务排障,也能显著提升单体

weixin_42525551的博客 305

gin 框架原理

Gin的路由原理 Gin的路由基于Trie树和压缩字典树算法,什么是Trie树?其实很好理解,看下图: 单词at,bee,ben,bt,q组成的Trie树如下: 每个字母的父亲节点就是它的前一个字母 Trie树的三个性质: 根节点不包含字符,除根节点外每一个节点都只包含一个字符 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串 每个节点的所有子节点包含的字符都不相同 也就是按照各个词的前缀,将所有的词拆解组成一颗树的结构。 那么有了这样的一颗树,查找单词就变得很简单,从根节点开始向下

衡与墨的博客 1659

【Go项目实战】之WEB博客管理系统

模板讲解,登录及分类,文章添加标签,评论与分类显示,附件上传,国际化支持等等

Go框架解析:gin

转载自http://tigerb.cn/2019/07/06/go-gin/ Go框架解析:gin 2019-07-06 前言 今天是我golang框架阅读系列第三篇文章,今天我们主要看看gin的框架执行流程。关于golang框架生命周期源码阅读下面是我的计划: 计划 状态 Go框架解析:beego ✅done Go框架解析:iris ✅done Go框架解析:gin ✅done Go框架解析:echo ✅done Go框架解析:...

Shanks 954

K型热电偶负电压采集难题?3种运放电路设计方案对比(含Multisim仿真)

本文针对K型热电偶在低温测量中产生的负电压采集难题,详细对比了三种运放电路设计方案,包括双电源运放反相放大、单电源仪表放大器和差分放大电路,并结合Multisim仿真数据分析了各方案的性能差异。文章还提供了PCB布局和抗干扰设计的实战技巧,帮助工程师解决热电偶信号采集中的核心挑战。

postgres8guard的博客 1017

Oracle Patch清理

- opatch clean的帮助,该命令清除.patch_storage directory文件夹下的'restore.sh,make.txt' files and 'scratch,backup'-- 执行完毕后,貌似没有变化,可能和自己环境有关(19年开始测试补丁,基本上每个季度都测试,后面把又把数据库用RMAN做了迁移,又转换成了PDB模式)。在对Oracle安装补丁后,会发现OS上被占用了大量的空间,本文档清理Opatch过程中的一些文件,释放空间。--清理后空间,剩余25G。

文档搬运工 1068

Gin框架原理

Gin是Go语言写的一个web框架,它具有运行速度快,分组的路由器,良好的崩溃捕获和错误处理,非常好的支持中间件和json。Gin官网Gin是一个golang的微框架,封装比较优雅,API友好,源码注释比较明确,具有快速灵活,容错方便等特点。对于golang而言,web框架的依赖要远比Python,Java之类的要小。自身的net/http足够简单,性能也非常不错。借助框架开发,不仅可以省去很多常用的封装带来的时间,也有助于团队的编码风格和形成规范。.........

追求幸福,探索未知的博客 1596

ZYNQ开发(2)在Linux系统层上PL-PS通过AXI-DMA传输数据详细步骤及注意事项

参考的网络资料 https://github.com/bperez77/xilinx_axidma/tree/master https://gitlab.gnome.org/HosseinBeheshti/UlteraZed7EV/-/tree/master/petalinux/xilinx_axidma https://www.icode9.com/content-3-925319.html https://blog.youkuaiyun.com/weixin_38218267/article/details/1

aizaiyueye的专栏 3448

Gin框架:性能强悍的秘密武器与底层技术原理揭秘

Gin框架之所以性能强悍,是由于其高效的HTTP引擎、轻量级设计、中间件支持、错误处理机制和并发处理能力等多个方面的综合作用。这些优点使得Gin框架在处理高并发请求时具有出色的性能表现,成为构建高性能Web应用程序的首选框架之一。通过深入理解Gin框架的底层技术原理,我们可以更好地利用其性能优势,为用户提供更加快速和稳定的Web服务。

u013558123的博客 1424

Eplan P8 API二次开发--插入符号(Symbol)功能(Function)并定义属性

1、目的 使用Eplan API在页中自动插入某个符号并按约定规则自动设置好属性 2. 步骤 一、基础信息 比如要在Eplan中自动插入一个电机的符号并定制属性,我们先要确定要插入的符号名,可以在Eplan中获取如下符号(Symbol)信息 可以知道我们待插入的符号名称为M3_1, 接下来,使用Visual Studio建立类库项目,并引用Eplan Api库 二、获取符号变量(SymbolVariant) 在Eplan中,当一个符号(Symbol)被插入到页中,这个符号的引用即变为一个(Function

如是道程序人生 4523

Adobe Acrobat Pro(Adobe PDF) - 使用技巧

Adobe Acrobat Pro为Adobe出品的PDF查看和编辑软件,下文简称Adobe PDF,该软件功能强大,拥有查看PDF、编辑PDF、打印PDF、导出PDF、合并文件、组织页面、文本标注、注释、动作向导、等功能。本文介绍Adobe PDF的一些使用技巧,最后介绍软件设置。

NSJim的博客 4万+

gin框架原理详解

gin作为web框架,在社区中拥有较高的活跃度,通过使用前缀树结构存储,减少内存消耗,同时加快了路由查询速度,并且在基本功能的基础上提供了各种middleware,可以在访问对应的url时回调对应的handler...

qq_37280513的博客 2532

运放实现阶梯波发生器-总结报告

实验目的: 学习、理解、掌握方波电路、限幅电路积分累加电路、反馈电路 课程条件: 电脑仿真软件Multisim 原理框图和实验思路: 方波电路通过微分电路可以形成正负脉冲,在通过限幅电路后变为单脉冲,最后在积分电路中形成阶梯波。微分电路存在的意义就是为了防止方波频率过大,从而导致在经过限幅电路的方波,在一个周期内将电容的电充满,这样就不会产生阶梯波。如果方波的频率比较小,可以近似认为是正负脉冲,这样就可以跳过微分电路。 ...............

0 1万+

基于Golang的开发框架Gin实战

这篇文章是关于基于 Golang 的 Gin 框架开发的实战记录,面向前端想进阶服务端开发的小伙伴。介绍了项目结构、包依赖、配置文件、数据库、路由、控制器、视图、数据模型等,还分享了一些学习和开发的反思与观点,如技术变现、时间利用等。最后给出了运行项目和测试截图。

sinat_33370468的博客 1572

基于STM32设计的猪舍养殖的监控系统_325

本课题基于STM32单片机,设计一款能够实时监测猪舍环境的智能监控系统。该系统能够通过温湿度、空气质量、光照强度等传感器实时监测猪舍的环境,并能够通过自动化控制设备(如风扇、照明灯、消毒喷雾等)调节环境参数,确保养殖环境的适宜性。同时,系统还支持通过ESP8266模块实现数据上传与远程控制,使养殖管理者能够通过手机APP或计算机远程监控和控制猪舍环境。

408

【Dify系列教程重置精品版】第二章:在Windows上部署Dify

让我们的电脑允许使用Linux系统。有点兴奋是怎么回事?简单来说就是让你的电脑拥有另一个叫做Linux的系统。3.选择“适用于Linux的Windows子系统”与 “虚拟机平台”与“Hyper-V"第1章:安装 wsl (Windows Subsystem for Linux)上一章我们聊了什么是Dify,这一章我们讲一下怎么才能用Dify。好了,我们的环境部署好了,下一章我们正式进入Dify。下载完成后,我们直接一直无脑下一步,安装即可。完成上述步骤,我们就可以开始安装wsl了。我们的虚拟化也是开启的。

xiezhipu的博客 866

7days-golang Web框架Gee完全指南:如何模仿Gin在7天内实现一个自己的Go Web框架

7days-golang 是一个「7天用Go动手从零实现」的开源项目,其中的 **Gee** 模块模仿 [Gin] 的设计,用 7 天的时间带你从零实现一个功能完整的 Go Web 框架,涵盖动态路由、路由分组、中间件、HTML 模板和 panic 恢复等 Gin 的核心能力。本指南面向新手,用最少的代码讲清每一天的设计思路,帮你快速理解「一个 Web 框架到底是怎么工作的」。 [![7days

gitblog_01155的博客 516

霍尼韦尔SoftMaster使用说明

霍尼韦尔SoftMaster使用说明

7days-golang项目全解析:7天用Go从零实现4大项目,一次看懂Web框架、分布式缓存、ORM与RPC

**7days-golang** 是一个帮助新手通过动手写代码来深入理解 Go 语言的开源仓库:每个项目只用 7 天时间,从第一行代码开始,从零实现 Web 框架、分布式缓存、ORM 框架与 RPC 框架四大经典基础设施。每天约 100 行代码,每天的功能都可以独立运行和测试,就像搭积木一样层层叠加,是学习 Go 并发编程与分布式系统设计的极佳入门路径 🧩。 ## 为什么"7天从零实现"值得学

gitblog_00654的博客 961

pscad IEEE标准系统搭建说明

pscad IEEE标准系统搭建说明,包含典型的IEEE 9节点、14节点、30节点、39节点和118节点系统。内含标准测试系统的原始数据和潮流结果。

Camera2Demo

在Camera2 的基础上进行封装,使得Camera2一堆初始化逻辑与Activity分离,降低耦合度,完成预览拍照并保存的操作

Gin Web Framework 实战指南:为 FIND 室内定位服务构建高性能 Go Web 后端

Gin 是 Go 语言(Golang)生态中最具代表性的高性能 Web 框架之一,它以 martini 风格的精简 API、近乎零分配的 radix 树路由器和完整的中件间/绑定/渲染体系著称。本指南以本仓库内 [vendor/github.com/gin-gonic/gin/README.md](https://link.gitcode.com/i/53fb845922a52b530090b9f

gitblog_00781的博客 206

android 蓝牙通讯demo

android 下蓝牙通讯demo,实现了发现蓝牙,使能蓝牙,通过蓝牙发送消息。

《Go语言高级编程》中间件实战:用 http.Handler 链式解耦 Go Web 业务与非业务代码

**本篇技术指南以《Go语言高级编程》开源图书 [第 5.3 节「中间件」](https://link.gitcode.com/i/037e894b2773bf9bd6286f0ddb8988d2) 为核心,系统讲解 Go Web 中间件(middleware)的设计原理与工程实践。** 文章从"业务代码与非业务代码揉成一团"的典型泥潭出发,逐步推导出基于 `http.Handler` 的 fun

gitblog_00267的博客 860

用 OpenCore Legacy Patcher 把老 Mac 升级 macOS:完整操作与排障速查

一台 2007 到 2017 年间的 Intel Mac 被苹果移出升级名单,"系统设置"里找不到新 macOS 的入口,软件版本被锁在旧档,这是这篇文章处理的场景。OpenCore Legacy Patcher(下文简称 OCLP)在老 Mac 前面装一层 OpenCore 引导,负责绕过硬件检查、注入驱动、修补内核扩展,让最新 macOS 在旧硬件上跑起来。下文按 原理 → 资格 → 准备 →

gitblog_00770的博客 353

Go语言后端开发实战指南:构建高性能云原生服务

本文将深入探讨Go语言在后端开发中的技术优势、核心生态和实战技巧,帮助开发者构建高性能、易维护的后端服务。

ITOfDragon的博客 729

Qwerty Learner 自定义词典导入:3 步拿到你的专属打字练习词库

中文打字飞快、英文打字卡壳,是 Qwerty Learner 想解决的典型问题。它把背单词和英语打字肌肉记忆练习合在同一个界面里,内置词库不够用时,就可以用自定义词典导入功能把自己的词放进去。下文带你完整走一遍:准备 JSON 文件、注册索引、启动验证,三步完成。 ## 功能速览:Qwerty Learner 自定义词典导入适合谁 先明确这个功能能为你做什么: - **它是什么**:开源的打

gitblog_00452的博客 450

go-admin 权限管理系统实战指南:基于 Gin + Vue 的前后端分离中后台脚手架

> 本文以 [README.zh-TW.md](https://link.gitcode.com/i/da392837021e04bbb36ff9ab0493695a) 为骨架,结合当前仓库的源码实现(命令入口、迁移引擎、配置体系、权限中间件等)展开,系统讲解 go-admin 的核心特性、内建功能、本地开发流程与底层原理,帮助读者在一小时内把项目跑起来,并理解它的运行机制。 ## 项目定位与导

gitblog_00763的博客 382

Martini 实战指南:用 Go 构建模块化 Web 应用(路由、依赖注入与中间件)

Martini 是一个用于在 Golang 中快速编写模块化 Web 应用/服务的轻量级框架,本文以仓库内的西班牙语文档 [translations/README_es_ES.md](https://link.gitcode.com/i/052a5853ea4336f73f52e973e709d563) 为主体脉络,结合 [martini.go](https://link.gitcode.com/

gitblog_00329的博客 84

Go 夜读 go mod 源码阅读(二):旧版依赖配置迁移与 modfile 转换机制深度解析

本文是 Go 夜读「Go 标准包阅读」系列的第 27 期分享笔记,基于 Go 1.11.5 源码,聚焦 `go mod` 工具链中两个容易被忽略却十分关键的实现细节:`modfile.File` 对象在旧版模块配置迁移中的落点作用,以及 `ConvertLegacyConfig` 如何把 glock、godep、dep、glide、govendor 等历史依赖管理工具的配置文件统一转换为 go.m

gitblog_00564的博客 87
上一篇: go语言简单实现 前缀树(Trie)与了解基数树(Radix-Tree)
下一篇: (1)go web开发之 zap日志库的使用及gin框架配置zap记录日志详细文档讲解分析
我是py呀
博客等级 码龄8年 205粉丝 130原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值