百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 热门文章 > 正文

深入浅出 一文带你了解Gin 生命周期

bigegpt 2024-08-09 11:09 2 浏览

Gin 是一个用 Go (Golang) 编写的 web 框架,由于出色的性能优势而被广泛使用,这里我们就来分析下 Gin 的请求生命周期

1 Gin 目录结构

先来了解下其目录结构:

.
├── binding 依据 HTTP 请求 Accept 解析响应数据格式
│   ├── binding.go
│   ├── binding_nomsgpack.go
│   ├── default_validator.go
│   ├── form.go
│   ├── form_mapping.go
│   ├── header.go
│   ├── json.go
│   ├── msgpack.go
│   ├── multipart_form_mapping.go
│   ├── protobuf.go
│   ├── query.go
│   ├── uri.go
│   ├── xml.go
│   ├── yaml.go
├── ginS
│   └── gins.go
├── internal
│   ├── bytesconv
│   │   ├── bytesconv.go
│   └── json
│       ├── json.go
│       └── jsoniter.go
├── render 依据解析的 HTTP 请求 Accept 响应格式生成响应
│   ├── data.go
│   ├── html.go
│   ├── json.go
│   ├── msgpack.go
│   ├── protobuf.go
│   ├── reader.go
│   ├── redirect.go
│   ├── render.go
│   ├── text.go
│   ├── xml.go
│   └── yaml.go
├── auth.go
├── *context.go
├── context_appengine.go
├── debug.go
├── deprecated.go
├── errors.go
├── fs.go
├── *gin.go
├── logger.go
├── mode.go 设置 Gin 运行环境模式
├── path.go Path 处理
├── recovery.go 处理 Panic 的 Recovery 中间件
├── *response_writer.go ResponseWriter
├── *routergroup.go 路由组设置
├── tree.go 路由算法
├── utils.go helper 函数
└── version.go

其中比较重要的模块为: context.go,gin.go,routergroup.go,以及 tree.go;分别处理 HTTP 请求及响应上下文,gin 引擎初始化,路由注册及路由查找算法实现。

binding 目录内提供基于 HTTP 请求消息头 Context-Type 的 MIME 信息自动解析功能,相对应的 Render 目录下提供具体数据格式渲染的实现方法。

2 Gin 请求生命周期

本文着重介绍 Gin 实现一个 Web 服务器,从请求到达到生成响应整个生命周期内的核心功能点,这将有助于我们理解 Gin 的执行原理和以后的开发工作的展开。

2.1 简单了解下 Gin 服务执行流程

先从官网的第一个 demo example.go 出发:

package main

import "github.com/gin-gonic/gin"

func main() {
    // 创建 Gin Engine 实例
    r := gin.Default()

    // 设置请求 URI /ping 的路由及响应处理函数
    r.GET("/ping", func(c *gin.Context) {
        c.JSON(200, gin.H{
            "message": "pong",
        })
    })

    // 启动 Web 服务,监听端口,等待 HTTP 请求到并生成响应
    r.Run() // 监听并在 0.0.0.0:8080 上启动服务
}

通过执行 go run example.go 命令来运行代码,它会启动一个阻塞进程监听并等待 HTTP 请求:

# 运行 example.go 并且在浏览器中访问 0.0.0.0:8080/ping
$ go run example.go

从代码中我们可以看出通过 Gin 实现一个最简单的 Web 服务器,只需 3 个步骤:

1)创建 Gin 实例
2)注册路由及处理函数
3)启动 Web 服务

2.2 Gin 生命周期

2.2.1 创建 Gin 实例

Gin 实例创建通过 gin.Default() 方法完成,其定义在 gin.go#L159 文件里:

// 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
}

Default() 方法实现如下功能:
1)创建 Gin 框架对象 Engine
2)配置 Gin 默认的中间件,Logger() 和 Recovery(),其实现分别位于 logger.go 和 recovery.go 文件内
3)返回 Gin 框架对象

其中 New() 方法会实例化 Gin 的 Engine 对象,gin.go#L129

// New returns a new blank Engine instance without any middleware attached.
// By default the configuration is:
// - RedirectTrailingSlash:  true
// - RedirectFixedPath:      false
// - HandleMethodNotAllowed: false
// - ForwardedByClientIP:    true
// - UseRawPath:             false
// - UnescapePathValues:     true
func New() *Engine {
    debugPrintWARNINGNew()
    engine := &Engine{
        RouterGroup: RouterGroup{
            Handlers: nil,
            basePath: "/",
            root:     true,
        },
        FuncMap:                template.FuncMap{},
        RedirectTrailingSlash:  true,
        RedirectFixedPath:      false,
        HandleMethodNotAllowed: false,
        ForwardedByClientIP:    true,
        AppEngine:              defaultAppEngine,
        UseRawPath:             false,
        RemoveExtraSlash:       false,
        UnescapePathValues:     true,
        MaxMultipartMemory:     defaultMultipartMemory,
        trees:                  make(methodTrees, 0, 9),
        delims:                 render.Delims{Left: "{{", Right: "}}"},
        secureJSONPrefix:       "while(1);",
    }
    engine.RouterGroup.engine = engine
    engine.pool.New = func() interface{} {
        return engine.allocateContext()
    }
    return engine
}

实例化比较核心的功能是:
1)初始化 Engine 对象, 关键步骤是初始化路由组 RouterGroup。
2)初始化 pool, 这是核心步骤. pool 用来存储 context 上下文对象. 用来优化处理 http 请求时的性能。

后面会重点分析 engine.pool 的实现细节。

Engine 是 Gin 框架的核心引擎 gin.go#L56,数据结构如下:

type Engine struct {
    RouterGroup // 关键:路由组

    // 设置开关
    RedirectTrailingSlash bool
    RedirectFixedPath bool
    HandleMethodNotAllowed bool
    ForwardedByClientIP    bool
    
    AppEngine bool
    UseRawPath bool
    
    UnescapePathValues bool
    MaxMultipartMemory int64
    RemoveExtraSlash bool

    // 界定符
    delims           render.Delims
    secureJSONPrefix string
    HTMLRender       render.HTMLRender
    FuncMap          template.FuncMap
    allNoRoute       HandlersChain
    allNoMethod      HandlersChain
    noRoute          HandlersChain
    noMethod         HandlersChain
    
    pool             sync.Pool // 关键:context 处理
    
    trees            methodTrees
    maxParams        uint16
}

Engine 结构体内部除一些功能性开关设置外,核心的就是 RouterRroup,pool 和 trees。Gin 的所有组件都是由 Engine 驱动。

2.2.2 路由注册

完成 Gin 的实例化之后,我们可以通过 r.GET("/ping", func(c *gin.Context) {}) 定义 HTTP 路由及处理 handler 函数。

以 gin.GET 为例,展开源码如下:

// 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)
}

gin.GET 定义 HTTP GET 请求的路由及处理方法,并返回 IRoutes 对象实例。

2.2.2.1 RouterGroup 结构体

// RouterGroup is used internally to configure router, a RouterGroup is associated with
// a prefix and an array of handlers (middleware).
type RouterGroup struct {
    Handlers HandlersChain
    basePath string
    engine   *Engine
    root     bool
}

RouterGroup routergroup.go#L41 用于配置路由,其中:

  • Handlers 数组定义了 Gin 中间件调用的 handler 方法
  • engine 为 gin.go 实例化时设置的 Engine 实例对象

2.2.2.2 handle 添加路由

gin.GET 方法内部通过调用 group.handle() routergroup.go#L72 方法添加路由:

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()
}

路由就和 Engine 绑定好关系了。

2.2.2.3 IRoute 接口类型

// IRoutes defines all router handle interface.
type IRoutes interface {
    Use(...HandlerFunc) IRoutes

    Handle(string, string, ...HandlerFunc) IRoutes
    Any(string, ...HandlerFunc) IRoutes
    GET(string, ...HandlerFunc) IRoutes
    POST(string, ...HandlerFunc) IRoutes
    DELETE(string, ...HandlerFunc) IRoutes
    PATCH(string, ...HandlerFunc) IRoutes
    PUT(string, ...HandlerFunc) IRoutes
    OPTIONS(string, ...HandlerFunc) IRoutes
    HEAD(string, ...HandlerFunc) IRoutes

    StaticFile(string, string) IRoutes
    Static(string, string) IRoutes
    StaticFS(string, http.FileSystem) IRoutes
}

IRoute 是个接口类型,定义了 router 所需的 handle 接口,RouterGroup 实现了这个接口。

2.2.2.4 小结

推而广之,Gin 还支持如下等路由注册方法:

  • r.POST
  • r.DELETE
  • r.PATCH
  • r.PUT
  • r.OPTIONS
  • r.HEAD
  • 以及 r.Any

它们都定义在 routergroup.go 文件内。

2.2.3 接收请求并响应

Gin 实例化和路由设置后工作完成后,我们进入 Gin 生命周期执行的核心功能分析,Gin 究竟是如何启动 Web 服务,监听 HTTP 请求并执行 HTTP 请求处理函数生成响应的。这些工作统统从 gin.Run() 出发 gin.go#L305:

// 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) }()

    address := resolveAddress(addr)
    debugPrint("Listening and serving HTTP on %s\n", address)
    err = http.ListenAndServe(address, engine)
    return
}

gin.Run() 是 net/http 标准库 http.ListenAndServe(addr, router) 的简写,功能是将路由连接到 http.Server 启动并监听 HTTP 请求。

由此,我们不得不放下手头的工作,率先了解下 net/http 标准库的执行逻辑。

2.2.3.1 net/http 标准库

net/http 标准库的 ListenAndServe(addr string, handler Handler) 方法定义在 net/http/server.go#L3162 文件里。

  • 参数签名的第一个参数是监听的服务地址和端口;
  • 第二个参数接收一个 Handler 对象它是一个接口类型需要实现 ServeHTTP(ResponseWriter, *Request) 方法。
func ListenAndServe(addr string, handler Handler) error {
    server := &Server{Addr: addr, Handler: handler}
    return server.ListenAndServe()
}

ListenAndServe(addr string, handler Handler) 内部则调用的是 Server 对象的 ListenAndServe() 方法由交由它启动监听和服务功能:

// ListenAndServe listens on the TCP network address srv.Addr and then
// calls Serve to handle requests on incoming connections.
// Accepted connections are configured to enable TCP keep-alives.
//
// If srv.Addr is blank, ":http" is used.
//
// ListenAndServe always returns a non-nil error. After Shutdown or Close,
// the returned error is ErrServerClosed.
func (srv *Server) ListenAndServe() error {
    if srv.shuttingDown() {
        return ErrServerClosed
    }
    addr := srv.Addr
    if addr == "" {
        addr = ":http"
    }
    ln, err := net.Listen("tcp", addr)// 监听
    if err != nil {
        return err
    }
    return srv.Serve(ln)// 启动服务等待连接
}

然后,执行 srv.Serve(ln)Server.Serve(l net.Listener) server.go#L2951,在 net.Listen("tcp", addr) 等待连接,创建新的 goroutine 来处理请求和生成响应的业务逻辑:

func (srv *Server) Serve(l net.Listener) error {
    ...
    for {
        rw, e := l.Accept()
        if e != nil {
            select {
            case <-srv.getDoneChan():
                return ErrServerClosed
            default:
            }
            ...
            return e
        }
        if cc := srv.ConnContext; cc != nil {
            ctx = cc(ctx, rw)
            if ctx == nil {
                panic("ConnContext returned nil")
            }
        }
        tempDelay = 0
        c := srv.newConn(rw)
        c.setState(c.rwc, StateNew) // before Serve can return
        go c.serve(ctx) // 启动 Web 服务
    }
}

最后,进入到 go c.serve(ctx) 启动 Web 服务,读取 HTTP 请求数据,生成响应 server.go#L1817:

// Serve a new connection.
func (c *conn) serve(ctx context.Context) {
    ...

    // HTTP/1.x from here on.
    for {
        w, err := c.readRequest(ctx)// 读取 HTTP 去请求

        ...

        // HTTP cannot have multiple simultaneous active requests.[*]
        // Until the server replies to this request, it can't read another,
        // so we might as well run the handler in this goroutine.
        // [*] Not strictly true: HTTP pipelining. We could let them all process
        // in parallel even if their responses need to be serialized.
        // But we're not going to implement HTTP pipelining because it
        // was never deployed in the wild and the answer is HTTP/2.
        serverHandler{c.server}.ServeHTTP(w, w.req)
        ...
    }
}

最终,调用 r.Run() 方法传入的 Engine 来执行 serverHandler{c.server}.ServeHTTP(w, w.req) 处理接收到的 HTTP 请求和生成响应,这里将响应处理的控制权交回给 Gin Engine。

小结

Go 标准库 net/http 提供了丰富的 Web 编程接口支持,感兴趣的朋友可以深入研究下 net/http 标准库源码,了解其实现细节。

2.2.3.2 Engine.ServeHTTP 处理 HTTP 请求

Engine.ServeHTTP 是 Gin 框架核心中的核心,我们来看下它是如何处理请求和响应的:

// ServeHTTP conforms to the http.Handler interface.
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    c := engine.pool.Get().(*Context) // 从临时对象池 pool 获取 context 上下文对象
    c.writermem.reset(w)
    c.Request = req
    c.reset()

    engine.handleHTTPRequest(c) // 处理 HTTP 请求

    engine.pool.Put(c) // 使用完 context 对象, 归还给 pool 
}

ServeHTTP会先获取 Gin Context 上下文信息,接着将 Context 注入到 engine.handleHTTPRequest(c) 方法内来处理 HTTP 请求:

func (engine *Engine) handleHTTPRequest(c *Context) {
    httpMethod := c.Request.Method
    rPath := c.Request.URL.Path

    ...

    // Find root of the tree for the given HTTP method
    t := engine.trees
    for i, tl := 0, len(t); i < tl; i++ {

        ...

        root := t[i].root
        // Find route in tree
        value := root.getValue(rPath, c.params, unescape)
        
        ...
        
        if value.handlers != nil {
            c.handlers = value.handlers
            c.fullPath = value.fullPath
            c.Next() // 具体执行响应处理
            c.writermem.WriteHeaderNow()
            return
        }
        if httpMethod != "CONNECT" && rPath != "/" {
            ...
        }
        break
    }

    ...
}

handleHTTPRequest 完成 路由回调 方法的查找,执行 Gin.Context.Next() 调用处理响应。

2.2.3.3 Gin.Context.Next() 在内部中间件执行 handler 方法

Gin.Context.Next() 仅有数行代码:

// 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++
    }
}

功能是在 Gin 内部中间件中执行 handler 调用,即 r.GET() 中传入的

func(c *gin.Context) {
    c.JSON(200, gin.H{
        "message": "pong",
    })
}

方法生成 HTTP 响应。

到这里我们完成了 Gin 的请求和响应的完整流程的源码走读,但是我们有必要对 Gin.Context 有多一些的了解。

2.2.3.4 Gin.Context 上下文处理

Gin 的 Context 实现了对 request 和 response 的封装是 Gin 的核心实现之一,其数据结构如下:

// Context is the most important part of gin. It allows us to pass variables between middleware,
// manage the flow, validate the JSON of a request and render a JSON response for example.
type Context struct {
    writermem responseWriter

    Request   *http.Request    // HTTP 请求
    Writer    ResponseWriter   // HTTP 响应

    Params   Params
    handlers HandlersChain  // 关键: 数组: 内包含方法集合
    index    int8

    engine *Engine  // 关键: 引擎

    // Keys is a key/value pair exclusively for the context of each request.
    Keys map[string]interface{}

    // Errors is a list of errors attached to all the handlers/middlewares who used this context.
    Errors errorMsgs

    // Accepted defines a list of manually accepted formats for content negotiation.
    Accepted []string
}

其包含了 Gin 请求及响应的上下文信息和 Engine 指针数据

  • Request *http.Request : HTTP 请求
  • Writer ResponseWriter : HTTP 响应
  • handlers HandlersChain : 是 type HandlerFunc func(*Context) 方法集即路由设置的回调函数
  • engine *Engine : gin框架对象

Gin 官方文档 几乎所有的示例都是在讲解 Context 的使用方法,可用说研究 Context 源码对用好 Gin 框架会起到只管重要的作用。

相关推荐

【Docker 新手入门指南】第十章:Dockerfile

Dockerfile是Docker镜像构建的核心配置文件,通过预定义的指令集实现镜像的自动化构建。以下从核心概念、指令详解、最佳实践三方面展开说明,帮助你系统掌握Dockerfile的使用逻...

Windows下最简单的ESP8266_ROTS_ESP-IDF环境搭建与腾讯云SDK编译

前言其实也没啥可说的,只是我感觉ESP-IDF对新手来说很不友好,很容易踩坑,尤其是对业余DIY爱好者搭建环境非常困难,即使有官方文档,或者网上的其他文档,但是还是很容易踩坑,多研究,记住两点就行了,...

python虚拟环境迁移(python虚拟环境conda)

主机A的虚拟环境向主机B迁移。前提条件:主机A和主机B已经安装了virtualenv1.主机A操作如下虚拟环境目录:venv进入虚拟环境:sourcevenv/bin/active(1)记录虚拟环...

Python爬虫进阶教程(二):线程、协程

简介线程线程也叫轻量级进程,它是一个基本的CPU执行单元,也是程序执行过程中的最小单元,由线程ID、程序计数器、寄存器集合和堆栈共同组成。线程的引入减小了程序并发执行时的开销,提高了操作系统的并发性能...

基于网络安全的Docker逃逸(docker)

如何判断当前机器是否为Docker容器环境Metasploit中的checkcontainer模块、(判断是否为虚拟机,checkvm模块)搭配学习教程1.检查根目录下是否存在.dockerenv文...

Python编程语言被纳入浙江高考,小学生都开始学了

今年9月份开始的新学期,浙江省三到九年级信息技术课将同步替换新教材。其中,新初二将新增Python编程课程内容。新高一信息技术编程语言由VB替换为Python,大数据、人工智能、程序设计与算法按照教材...

CentOS 7下安装Python 3.10的完整过程

1.安装相应的编译工具yum-ygroupinstall"Developmenttools"yum-yinstallzlib-develbzip2-develope...

如何在Ubuntu 20.04上部署Odoo 14

Odoo是世界上最受欢迎的多合一商务软件。它提供了一系列业务应用程序,包括CRM,网站,电子商务,计费,会计,制造,仓库,项目管理,库存等等,所有这些都无缝集成在一起。Odoo可以通过几种不同的方式进...

Ubuntu 系统安装 PyTorch 全流程指南

当前环境:Ubuntu22.04,显卡为GeForceRTX3080Ti1、下载显卡驱动驱动网站:https://www.nvidia.com/en-us/drivers/根据自己的显卡型号和...

spark+python环境搭建(python 环境搭建)

最近项目需要用到spark大数据相关技术,周末有空spark环境搭起来...目标spark,python运行环境部署在linux服务器个人通过vscode开发通过远程python解释器执行代码准备...

centos7.9安装最新python-3.11.1(centos安装python环境)

centos7.9安装最新python-3.11.1centos7.9默认安装的是python-2.7.5版本,安全扫描时会有很多漏洞,比如:Python命令注入漏洞(CVE-2015-2010...

Linux系统下,五大步骤安装Python

一、下载Python包网上教程大多是通过官方地址进行下载Python的,但由于国内网络环境问题,会导致下载很慢,所以这里建议通过国内镜像进行下载例如:淘宝镜像http://npm.taobao.or...

centos7上安装python3(centos7安装python3.7.2一键脚本)

centos7上默认安装的是python2,要使用python3则需要自行下载源码编译安装。1.安装依赖yum-ygroupinstall"Developmenttools"...

利用本地数据通过微调方式训练 本地DeepSeek-R1 蒸馏模型

网络上相应的教程基本都基于LLaMA-Factory进行,本文章主要顺着相应的教程一步步实现大模型的微调和训练。训练环境:可自行定义,mac、linux或者window之类的均可以,本文以ma...

【法器篇】天啦噜,库崩了没备份(天啦噜是什么意思?)

背景数据库没有做备份,一天突然由于断电或其他原因导致无法启动了,且设置了innodb_force_recovery=6都无法启动,里面的数据怎么才能恢复出来?本例采用解析建表语句+表空间传输的方式进行...