云原生时代Go最受欢迎Web开源框架Gin原理与实战

文章目录

  • 概述
    • 定义
    • 特点
    • 概览导图
  • 使用
    • 快速入门
    • HTTP 方法使用
    • 参数获取
    • 参数绑定
    • 自定义日志输出
    • 自定义中间件
    • 路由组
    • HTML渲染
    • 设置和获取Cookie
    • XML、YAML、ProtoBuf渲染
    • 使用BasicAuth中间件
    • 静态文件和BootStrap
    • 使用Session
    • 写入日志文件
  • 原理
    • 核心执行流程
    • 核心数据结构

概述

定义

Gin 官网地址 https://gin-gonic.com/ 源码release最新版本v1.9.1

Gin 官网文档地址 https://gin-gonic.com/docs/

Gin 源码地址 https://github.com/gin-gonic/gin

Gin是目前使用最广泛、最快的全功能web框架之一,采用Go语言(Golang)编写HTTP 服务,与它类似如martini-like API ,但Gin性能更好,基于httprouter其速度快了40倍。

Gin是一种用于构建Web应用程序的Go语言框架,具有高性能、易于使用、轻量级和灵活的特点。 Gin提供了许多功能,例如路由、中间件、错误处理等。同时,Gin还可以与许多其他Go语言库和框架无缝集成。 Gin的设计目的是提供一种快速、可靠和高效的方式来构建Web应用程序,以满足现代Web应用程序的需求;它的文档和社区支持也非常好,因此它成为了Go语言中最受欢迎的Web框架之一。

特点

  • 快:基于Radix tree的路由,内存占用小;没有反射,可预测的API性能。
  • 中间件支持:传入的HTTP请求可以由一系列中间件和最终操作来处理。例如:Logger, Authorization, GZIP,最后在DB中发布消息。
  • 无故障:Gin可以捕获HTTP请求期间发生的panic并恢复它保证服务器的可用性;例如可以向哨兵报告这种panic。
  • JSON验证:Gin可以解析和验证请求的JSON,例如检查是否存在所需值。
  • 路由分组:更好地组织路由,如需要授权与不需要授权、不同的API版本;此外路由组可以无限嵌套且不会降低性能。
  • 错误管理:Gin提供了一种方便的方法来收集HTTP请求期间发生的所有错误。最终,中间件可以将它们写入日志文件、数据库并通过网络发送。
  • 呈现内置:Gin为JSON、XML和HTML渲染提供了一个易于使用的API。
  • 可扩展的:非常简单通过自定义创建新的中间件实现扩展功能。

概览导图

云原生时代Go最受欢迎Web开源框架Gin原理与实战_第1张图片

使用

快速入门

前置环境:需要go 1.13及以上版本

# 下载并安装
go get -u github.com/gin-gonic/gin

创建quick_start.go文件,导入gin库

package main

import (
	"github.com/gin-gonic/gin"
	"net/http"
)

func main() {
	// 创建一个带有默认中间件(logger and recovery (crash-free) 中间件)的gin.Engine
	r := gin.Default()
	// 配置路由映射
	r.GET("/hello", func(c *gin.Context) {
		c.String(http.StatusOK, "hello,welcome to go world!")
	})
    // 监听端口,默认为0.0.0.0:8080
	r.Run() 
}

启动运行,访问http://localhost:8080/hello可以看到成功返回信息

go run quick_start.go

云原生时代Go最受欢迎Web开源框架Gin原理与实战_第2张图片

HTTP 方法使用

package main

import (
	"github.com/gin-gonic/gin"
	"net/http"
)

func demoGet(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H{
		"message": "demo get",
	})
}

func demoPost(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H{
		"message": "demo post",
	})
}

func demoPut(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H{
		"message": "demo put",
	})
}

func demoDelete(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H{
		"message": "demo delete",
	})
}

func demoPatch(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H{
		"message": "demo patch",
	})
}

func demoHead(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H{
		"message": "demo head",
	})
}

func demoOptions(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H{
		"message": "demo options",
	})
}

func main() {
	// 创建一个带有默认中间件(logger and recovery (crash-free) 中间件)的gin.Engine
	r := gin.Default()

	// 配置路由映射
	r.GET("/DemoGet", demoGet)
	r.POST("/DemoPost", demoPost)
	r.PUT("/DemoPut", demoPut)
	r.DELETE("/DemoDelete", demoDelete)
	r.PATCH("/DemoPatch", demoPatch)
	r.HEAD("/DemoHead", demoHead)
	r.OPTIONS("/DemoOptions", demoOptions)

	r.Run() // 监听端口,默认为0.0.0.0:8080
}

运行程序,通过HTTP请求工具如PostMan输入请求地址http://localhost:8080/DemoPost,返回预期结果

云原生时代Go最受欢迎Web开源框架Gin原理与实战_第3张图片

参数获取

package main

import (
	"github.com/gin-gonic/gin"
	"net/http"
)

func getVal(c *gin.Context) {
	name := c.Query("name")
	addr := c.DefaultQuery("addr", "home")
	c.JSON(http.StatusOK, gin.H{
		"name": name,
		"addr": addr,
	})
}
func postVal(c *gin.Context) {
	name := c.PostForm("name")
	addr := c.DefaultPostForm("addr", "home")
	c.JSON(http.StatusOK, gin.H{
		"name": name,
		"addr": addr,
	})
}
func restVal(c *gin.Context) {
	name := c.Param("name")
	addr := c.Param("addr")
	c.JSON(http.StatusOK, gin.H{
		"name": name,
		"addr": addr,
	})
}

func postMapVal(c *gin.Context) {
	ids := c.QueryMap("ids")
	names := c.PostFormMap("names")
	c.JSON(http.StatusOK, gin.H{
		"ids":   ids,
		"names": names,
	})
}

func main() {
	// 创建一个带有默认中间件(logger and recovery (crash-free) 中间件)的gin.Engine
	r := gin.Default()

	// 配置路由映射
	r.GET("/GetVal", getVal)
	r.POST("/PostVal", postVal)
	r.POST("/PostMapVal", postMapVal)
	r.POST("/RestVal/:name/:addr", restVal)

	r.Run() // 监听端口,默认为0.0.0.0:8080
}

运行程序,通过HTTP请求工具如PostMan输入请求地址http://localhost:8080/GetVal?name=itxiaoshen,返回预期结果

云原生时代Go最受欢迎Web开源框架Gin原理与实战_第4张图片

用Post提交方式,输入请求地址http://localhost:8080/PostVal并选择Body的中form-data或者x-www-form-urlencoded填写参数,返回预期结果

云原生时代Go最受欢迎Web开源框架Gin原理与实战_第5张图片

输入请求地址http://localhost:8080/PostMapVal?ids[a]=hello&ids[b]=world,在Body体填写相应的数组参数值,通过数组返回并输出预期结果

云原生时代Go最受欢迎Web开源框架Gin原理与实战_第6张图片

输入请求地址http://localhost:8080/RestVal/lixuefeng/qinghua,在url路径参数中输入相应的参数值,返回预期结果

云原生时代Go最受欢迎Web开源框架Gin原理与实战_第7张图片

参数绑定

package main

import (
	"github.com/gin-gonic/gin"
	"net/http"
)

type Person struct {
	Name    string `form:"name"`
	Address string `form:"addr"`
	Age     int
}

type PersonUri struct {
	Name    string `uri:"name" binding:"required"`
	Address string `uri:"addr" binding:"required"`
	Age     int    `uri:"age" binding:"required"`
}

func bindVal(c *gin.Context) {
	var person Person
	if c.ShouldBind(&person) == nil {
		c.JSON(http.StatusOK, gin.H{
			"name": person.Name,
			"addr": person.Address,
			"age":  person.Age,
		})
	}
}

func personMethod(c *gin.Context) {
	var person PersonUri
	if err := c.ShouldBindUri(&person); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"msg": err})
		return
	}
	c.JSON(http.StatusOK, gin.H{
		"name": person.Name,
		"addr": person.Address,
		"age":  person.Age,
	})
}

func main() {
	// 创建一个带有默认中间件(logger and recovery (crash-free) 中间件)的gin.Engine
	r := gin.Default()

	// 配置路由映射
	r.GET("/BindVal", bindVal)
	r.GET("/person/:name/:addr/:age", personMethod)

	r.Run() // 监听端口,默认为0.0.0.0:8080
}

运行程序,输入请求地址http://localhost:8080/BindVal?name=wangchuanjun&addr=zhengjiang&age=20,由于传入age字段与Person的Age字段名没匹配上,因此Age字段获取不到值采用的默认值0输出

云原生时代Go最受欢迎Web开源框架Gin原理与实战_第8张图片

通过uri路径参数输入请求地址http://localhost:8080/person/yangju/chengsan/26,返回预期结果

云原生时代Go最受欢迎Web开源框架Gin原理与实战_第9张图片

自定义日志输出

package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"net/http"
	"time"
)

func main() {
	r := gin.New()
	// LoggerWithFormatter中间件将把日志写入gin.DefaultWriter,默认为gin.DefaultWriter = os.Stdout
	r.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
		// 自定义输出日志
		return fmt.Sprintf("custom log format:%s -------- [%s] \"%s %s %d %s \"%s\" %s\"%s\n",
			param.ClientIP,
			param.Method,
			param.Path,
			param.Request.Proto,
			param.StatusCode,
			param.Latency,
			param.Request.UserAgent(),
			param.ErrorMessage,
			param.TimeStamp.Format(time.RFC1123),
		)
	}))
	r.Use(gin.Recovery())
	r.GET("/hello", func(c *gin.Context) {
		c.String(http.StatusOK, "hello,welcome to go world!")
	})
	r.Run(":8080")
}

启动运行,访问http://localhost:8080/hello,查看运行的控制台日志可以看到自定义日志输出

云原生时代Go最受欢迎Web开源框架Gin原理与实战_第10张图片

自定义中间件

package main

import (
	"github.com/gin-gonic/gin"
	"log"
	"net/http"
	"time"
)

// 返回gin.HandlerFunc函数
func Logger() gin.HandlerFunc {
	return func(c *gin.Context) {
		t := time.Now()

		// 设置 userId 变量
		c.Set("userId", "10001")

		// 请求前

		c.Next()

		// 请求后
		latency := time.Since(t)
		log.Print(latency)

		// access the status we are sending
		status := c.Writer.Status()
		log.Println(status)
	}
}

func main() {
	r := gin.New()
    // 使用自定义中间件
	r.Use(Logger())

	r.GET("/hello", func(c *gin.Context) {
		userId := c.MustGet("userId").(string)
		c.String(http.StatusOK, "userId="+userId)
	})

	// 监听 0.0.0.0:8080
	r.Run(":8080")
}

启动运行,访问http://localhost:8080/hello,查看运行的控制台日志可以看到输出响应的日志,也响应userId结果

云原生时代Go最受欢迎Web开源框架Gin原理与实战_第11张图片

路由组

路由组可以多级嵌套,实现细粒度控制

package main

import (
	"github.com/gin-gonic/gin"
	"net/http"
)

func demoGet(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H{
		"message": "demo get",
	})
}

func demoPost(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H{
		"message": "demo post",
	})
}

func main() {
	// 创建一个带有默认中间件(logger and recovery (crash-free) 中间件)的gin.Engine
	r := gin.Default()

	v1 := r.Group("/v1")
	{
		v1.GET("/DemoGet", demoGet)
		v1.POST("/DemoPost", demoPost)
	}
	v2 := r.Group("/v2")
	{
		v2.GET("/DemoGet", demoGet)
		v2.POST("/DemoPost", demoPost)
	}

	r.Run() // 监听端口,默认为0.0.0.0:8080
}

运行程序,访问http://localhost:8080/v2/DemoGet,返回预期结果

云原生时代Go最受欢迎Web开源框架Gin原理与实战_第12张图片

HTML渲染

package main

import (
	"github.com/gin-gonic/gin"
	"net/http"
)

func main() {
	router := gin.Default()
	router.LoadHTMLGlob("templates/*")
	// 可以通过下面LoadHTMLFiles加载指定的文件
	//router.LoadHTMLFiles("templates/index.html", "templates/index1.html")
	router.GET("/index", func(c *gin.Context) {
		c.HTML(http.StatusOK, "index.html", gin.H{
			"content": "my website begin",
		})
	})
	router.Run(":8080")
}

运行程序,访问http://localhost:8080/index,返回预期结果

云原生时代Go最受欢迎Web开源框架Gin原理与实战_第13张图片

设置和获取Cookie

package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
)

func main() {
	router := gin.Default()

	router.GET("/cookie", func(c *gin.Context) {

		cookie, err := c.Cookie("gin_cookie")

		if err != nil {
			cookie = "NotSet"
			c.SetCookie("gin_cookie", "test", 3600, "/", "localhost", false, true)
		}

		fmt.Printf("Cookie value: %s \n", cookie)
	})

	router.Run()
}

运行程序,访问http://localhost:8080/index,可以查看cookie信息

云原生时代Go最受欢迎Web开源框架Gin原理与实战_第14张图片

XML、YAML、ProtoBuf渲染

package main

import (
	"github.com/gin-gonic/gin"
	"github.com/gin-gonic/gin/testdata/protoexample"
	"net/http"
)

func main() {
	r := gin.Default()

	r.GET("/someXML", func(c *gin.Context) {
		c.XML(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK})
	})

	r.GET("/someYAML", func(c *gin.Context) {
		c.YAML(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK})
	})

	r.GET("/someProtoBuf", func(c *gin.Context) {
		reps := []int64{int64(1), int64(2)}
		label := "test"
		data := &protoexample.Test{
			Label: &label,
			Reps:  reps,
		}
		c.ProtoBuf(http.StatusOK, data)
	})

	r.Run(":8080")
}

运行程序,访问http://localhost:8080/someYAML,可以查看渲染内容

云原生时代Go最受欢迎Web开源框架Gin原理与实战_第15张图片

使用BasicAuth中间件

package main

import (
	"github.com/gin-gonic/gin"
	"net/http"
)

// 模拟一些私有数据
var secrets = gin.H{
	"jasper": gin.H{"email": "[email protected]", "phone": "18821212121"},
	"lili":   gin.H{"email": "[email protected]", "phone": "18821212122"},
	"sam":    gin.H{"email": "[email protected]", "phone": "18821212123"},
}

func main() {
	r := gin.Default()

	// 使用gin.BasicAuth()中间件进行分组,实际中这里应该是从数据库中获取,gin.Accounts是map[string]string
	authorized := r.Group("/admin", gin.BasicAuth(gin.Accounts{
		"jasper": "123456",
		"austin": "123456",
		"lili":   "123456",
		"sam":    "123456",
	}))

	authorized.GET("/secrets", func(c *gin.Context) {
		// 获取user,它是由BasicAuth中间件设置的
		user := c.MustGet(gin.AuthUserKey).(string)
		if secret, ok := secrets[user]; ok {
			c.JSON(http.StatusOK, gin.H{"user": user, "secret": secret})
		} else {
			c.JSON(http.StatusOK, gin.H{"user": user, "secret": "NO SECRET :("})
		}
	})

	// 监听 0.0.0.0:8080
	r.Run(":8080")
}

运行程序,浏览器上访问http://localhost:8080/admin/secrets,在弹出认证窗口上输入用户名密码如jasper/123456

云原生时代Go最受欢迎Web开源框架Gin原理与实战_第16张图片

点击登录按钮后则返回预期响应信息。

云原生时代Go最受欢迎Web开源框架Gin原理与实战_第17张图片

静态文件和BootStrap

# getbootstrap官网地址:https://getbootstrap.com
# 下载最新版本5.3.0bootstrap
wget https://github.com/twbs/bootstrap/releases/download/v5.3.0/bootstrap-5.3.0-dist.zip

解压目录下css和js目录拷贝到工程目录下,这里将css和js目录放在根目录下的assets目录下,然后创建main.go

package main

import (
	"github.com/gin-gonic/gin"
	"net/http"
)

func index(c *gin.Context) {
	c.HTML(http.StatusOK, "index.html", nil)
}

func main() {
	// 创建一个带有默认中间件(logger and recovery (crash-free) 中间件)的gin.Engine
	r := gin.Default()

	// 配置静态资源
	r.Static("/assets", "assets")
	r.Static("/favicon.ico", "assets/favicon.ico")

	// 配置路由映射
	r.GET("/index", index)

	//加载html模版文件
	r.LoadHTMLGlob("templates/*")

	r.Run() // 监听端口,默认为0.0.0.0:8080
}

这里从getbootstrap官网上找一个组件放到html文件中

云原生时代Go最受欢迎Web开源框架Gin原理与实战_第18张图片

在templates目录下创建index.html模版文件

DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <link rel="stylesheet" href="../assets/css/bootstrap.min.css">
    <script src="../assets/js/bootstrap.min.js">script>
    <title>静态资源及BootStrap示例title>
head>
<body>
<h1>
    <button type="button" class="btn btn-primary">Primarybutton>
    <button type="button" class="btn btn-secondary">Secondarybutton>
    <button type="button" class="btn btn-success">Successbutton>
    <button type="button" class="btn btn-danger">Dangerbutton>
    <button type="button" class="btn btn-warning">Warningbutton>
    <button type="button" class="btn btn-info">Infobutton>
    <button type="button" class="btn btn-light">Lightbutton>
    <button type="button" class="btn btn-dark">Darkbutton>
    <button type="button" class="btn btn-link">Linkbutton>
h1>
body>
html>

上面在html配置link和script时前面加了…/,这样才可以在IDE环境中定位到,总体目录结构如下

云原生时代Go最受欢迎Web开源框架Gin原理与实战_第19张图片

运行程序,访问http://localhost:8080/index ,已经成功加载到静态资源

云原生时代Go最受欢迎Web开源框架Gin原理与实战_第20张图片

使用Session

关于Session的可以使用第三方库的方式,在https://pkg.go.dev/上搜索go session,选择第一个github.com/gin-contrib/sessions,其支持多后端会话管理的Gin中间件包括cookie-based、Redis、memcached、MongoDB、GORM、memstore、PostgreSQL。

# 下载并安装
go get github.com/gin-contrib/sessions
# 在代码中导入
import "github.com/gin-contrib/sessions"
package main

import (
	"github.com/gin-contrib/sessions"
	"github.com/gin-contrib/sessions/cookie"
	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()
	store := cookie.NewStore([]byte("secret"))
	r.Use(sessions.Sessions("mysession", store))

	r.GET("/session", func(c *gin.Context) {
		session := sessions.Default(c)

		if session.Get("mykey") != "myvalue" {
			session.Set("mykey", "myvalue")
			session.Save()
		}

		c.JSON(200, gin.H{"mykey": session.Get("mykey")})
	})
	r.Run(":8080")
}

运行程序,访问http://localhost:8080/session ,成功返回预期结果

image-20230608105348115

上面示例使用的是cookie-based后端存储方式,下面则演示redis作为后端存储示例

package main

import (
	"github.com/gin-contrib/sessions"
	"github.com/gin-contrib/sessions/redis"
	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()
	store, _ := redis.NewStore(10, "tcp", "localhost:6379", "123456", []byte("secret"))
	r.Use(sessions.Sessions("myredissession", store))

	r.GET("/incr", func(c *gin.Context) {
		session := sessions.Default(c)
		var count int
		v := session.Get("mycount")
		if v == nil {
			count = 0
		} else {
			count = v.(int)
			count++
		}
		session.Set("mycount", count)
		session.Save()
		c.JSON(200, gin.H{"mycount": count})
	})
	r.Run(":8080")
}

项目目录下命令行执行go mod tidy,然后多次访问http://localhost:8080/incr,可以看到mycount一直在自增

image-20230608110332310

写入日志文件

package main

import (
	"github.com/gin-gonic/gin"
	"io"
	"os"
)

func main() {
	// 禁用控制台颜色,在将日志写入文件时不需要控制台颜色。
	gin.DisableConsoleColor()

	// 记录到文件
	f, _ := os.Create("my_app.log")
	gin.DefaultWriter = io.MultiWriter(f)

	// 如果需要同时将日志写入文件和控制台,请使用以下代码
	// gin.DefaultWriter = io.MultiWriter(f, os.Stdout)

	router := gin.Default()
	router.GET("/hello", func(c *gin.Context) {
		c.String(200, "welcome to go world!")
	})

	router.Run(":8080")
}

启动程序,多次访问http://localhost:8080/hello,可以看到在当前目录下生成了my_app.log文件,用户可以结合日志需要比如使用logrus和file-rotatelogs库实现。

云原生时代Go最受欢迎Web开源框架Gin原理与实战_第21张图片

原理

核心执行流程

Gin整体流程还是比较简单的,从上面使用代码示例也可以看到基础执行流程大致如下:

  • 创建并初始化Engine对象
  • 注册middleware(中间件)
  • 注册路由及处理函数
  • 服务端口监听

主要设计核心方法流程

  • gin.Default():gin的初始化方法,目的是为了创建整个引擎,并初始化相关参数如RouterGroup、pool等。

使用gin框架开发时一般情况下使用默认的engine即可,因为相对于直接使用gin.New()创建Engine对象,它只是多注册了两个中间件。

func Default() *Engine {
	debugPrintWARNINGDefault()
    // 创建引擎
	engine := New()
    // 默认使用Logger和Recovery
	engine.Use(Logger(), Recovery())
	return engine
}

云原生时代Go最受欢迎Web开源框架Gin原理与实战_第22张图片

  • router.Use():使用中间件的方法,将请求过程中需要调用的中间件放入到HandlersChain,这个是一个数组,比如在请求前后需要加入通用方法如鉴权、自定义日志格式等。
// Use将全局中间件附加到路由器上。也就是说,通过Use()附加的中间件将是包含在每个请求(甚至404、405、静态文件)的处理程序链中,例如记录器或错误管理中间件的正确位置。
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
	engine.RouterGroup.Use(middleware...)
	engine.rebuild404Handlers()
	engine.rebuild405Handlers()
	return engine
}

// 将中间件添加到路由组中
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
	group.Handlers = append(group.Handlers, middleware...)
	return group.returnObj()
}

image-20230608165518013

  • router.GET():构建url和具体处理请求的handle的关系了,其实目标很明确,就是要将这组关系存入到最终的trees中去。
// 链接group.handle快捷方式
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
	return group.handle(http.MethodGet, relativePath, handlers)
}

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

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)

	root := engine.trees.get(method)
	if root == nil {
		root = new(node)
		root.fullPath = "/"
		engine.trees = append(engine.trees, methodTree{method: method, root: root})
	}
	root.addRoute(path, handlers)

	// Update maxParams
	if paramsCount := countParams(path); paramsCount > engine.maxParams {
		engine.maxParams = paramsCount
	}

	if sectionsCount := countSections(path); sectionsCount > engine.maxSections {
		engine.maxSections = sectionsCount
	}
}

image-20230608171339533

  • router.Run():内部流程主要是调用golang中net/http包下的方法监听服务端口。
// Run将路由器附加到http上,服务器并开始监听和服务HTTP请求;也即是http.ListenAndServe快捷方式,对于除非发生错误,否则此方法将无限期地阻塞调用例程
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
}

image-20230608174116829

  • ServeHTTP:接收请求请求的处理在gin.go中的ServeHTTP,其处理http.Handler接口。其根据请求的url和method找到对应的handle去处理,通过数查找去。同时利用context进行参数传递,用c.Next进行递归遍历中间件的调用,handle是一个链式过程。
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	c := engine.pool.Get().(*Context)
	c.writermem.reset(w)
	c.Request = req
	c.reset()

	engine.handleHTTPRequest(c)

	engine.pool.Put(c)
}

image-20230608174812538

核心数据结构

在New()创建*Engine的方法中可以看到初始化重要信息,

云原生时代Go最受欢迎Web开源框架Gin原理与实战_第23张图片

其核心组成的重要结构如下

云原生时代Go最受欢迎Web开源框架Gin原理与实战_第24张图片

Engine是一个总的引擎,保存了各个组件的信息 ,其他组件信息如下:

  • RouterGroup是一个路由组,路由管理相关,保存了路由信息 。路由组的目的是为了实现配置的复用,相关的请求使用一组单独的middleware;gin.Engine对象本身就是一个路由组。
type RouterGroup struct {
   // 路由组处理函数链,其下路由的函数链将结合路由组和自身的函数组成最终的函数链
   Handlers HandlersChain
   // 路由组的基地址,一般是其下路由的公共地址
   basePath string
   // 路由组所属的Engine,这里构成了双向引用
   engine   *Engine
   // 该路由组是否位于根节点,基于RouterGroup.Group创建路由组时此属性为false
   root     bool
}

在RouterGroup数据结构有一个非常重要的成员字段HandlersChain(处理器链 ),用于收集该路由组下注册的middleware函数。在运行时,会按顺序执行HandlersChain中的注册的函数。

// HandlersChain 定义为一个HandlerFunc切片.
type HandlersChain []HandlerFunc
// HandlerFunc 定义gin中间件使用的处理程序作为返回值
type HandlerFunc func(*Context)
  • 路由树数组trees:trees是一棵树,保存了url与handle的映射关系 ,粗暴一点可以简单理解为key就是url字符串,value对应的[]HandleFunc;标准库本身的路由是不区分请求方法的,也就是说注册一个路由后,GET、POST都能匹配到该路由,而还需要在同一个路由在不同的请求方法下,由不同的逻辑进行处理。其实就是通过路由树实现的,gin的针对每个请求方法都有一棵路由树。Gin利用基于Radix Tree基数树思想通过优秀的数据结构和算法设计达到高性能目标。其核心实现是在gin的tree.go源码文件中。

    • Radix Tree是一种基于 Trie(字典树)的数据结构,旨在解决字符串搜索和匹配的问题。它最早由 Fredkin 在 1960 年提出,并在之后被广泛应用于各种应用领域。其最大的特点就是在 Trie 的基础上,加入了路径压缩的逻辑,通过合并前缀的方式大大的减少了 Trie 中的节点冗余问题,不仅提高了查询效率,还减少了存储空间的使用。
  • context对象池:engine中的pool用于复用Context,gin.Context是gin框架暴露给开发的另一个核心对象,可以通过该对象获取请求信息,业务处理的结果也是通过该对象写回客户端的。为了实现context对象的复用,gin基于sync.Pool实现了对象池。由于请求多会产生很多数量的context,利用pool来重复利用对象,从而减少内存的分配也提高了效率。

  • Context:包含了Request,Writer等信息,用于request中传递值。

  • 本人博客网站IT小神 www.itxiaoshen.com

你可能感兴趣的:(Go,云原生,云原生,golang,前端)