Gin框架原理

相关链接:
https://github.com/gin-gonic/gin
https://www.topgoer.cn/docs/ginkuangjia/

一、简介

Gin 是 Go语言写的一个 web 框架,它具有运行速度快,分组的路由器,良好的崩溃捕获和错误处理,非常好的支持中间件和 json。

Gin官网:Gin is a web framework written in Go (Golang). It features a martini-like API with performance that is up to 40 times faster thanks to httprouter. If you need performance and good productivity, you will love Gin.

  • Gin是一个golang的微框架,封装比较优雅,API友好,源码注释比较明确,具有快速灵活,容错方便等特点。

  • 对于golang而言,web框架的依赖要远比Python,Java之类的要小。自身的net/http足够简单,性能也非常不错。

  • 借助框架开发,不仅可以省去很多常用的封装带来的时间,也有助于团队的编码风格和形成规范。

二、安装与使用入门

安装:前提需要先安装好go1.15+,然后执行如下命令即完成安装。

go get -u github.com/gin-gonic/gin

使用启动一个gin服务,只需要简单的几行代码。

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

func main() {
	// 1.创建路由
	// gin 框架中采用的路由库是基于httprouter做的
	router := gin.Default()
	// 2.绑定路由规则,执行的函数
	// gin.Context,封装了request和response
	router.GET("/hello", func(c *gin.Context) {
		c.String(http.StatusOK, "hello World! I am gin!")
	})
	// 3.监听端口,默认在8080。Run("里面不指定端口号默认为8080")
	router.Run(":8080")
}

Gin支持路由、路由分组、中间件、会话控制、数据解析与绑定、参数验证、渲染等等功能,几乎能满足日常开发的所有需要。下面以路由分组、中间件举例,更多可参考文章开头的相关链接。

  • 路由分组
package main

import (
	"fmt"

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

func main() {
	// 1.创建路由
	// 默认使用了2个中间件Logger(), Recovery()
	r := gin.Default()
	// 路由组1 ,处理GET请求
	v1 := r.Group("/v1")
	// {} 是书写规范
	{
		v1.GET("/login", login)
		v1.GET("/submit", submit)
	}
	// 	路由组2 ,处理POST请求  【可以省略/】
	v2 := r.Group("v2")
	{
		v2.POST("login", login)
		v2.POST("submit", submit)
	}
	r.Run(":8000")
}

func login(c *gin.Context) {
	name := c.DefaultQuery("name", "tom")
	c.String(200, fmt.Sprintf("hello %s\n", name))
}

func submit(c *gin.Context) {
	name := c.DefaultQuery("name", "jerry")
	c.String(200, fmt.Sprintf("hello %s\n", name))
}
  • 中间件
package main

import (
	"fmt"
	"time"

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

func main() {
	router := gin.Default()
	router.Use(MiddleWare())
	router.GET("/middle", func(c *gin.Context) {
		// 取值
		req, _ := c.Get("request")
		fmt.Println("request:", req)
		// 页面接收
		c.JSON(200, gin.H{"request": req})
	})
	router.Run()
}

// 定义中间件
func MiddleWare() gin.HandlerFunc {
	return func(c *gin.Context) {
		t := time.Now()
		fmt.Println("中间件开始执行了")
		// 设置变量到Context的key中,可以通过Get()取
		c.Set("request", "我是中间件")
		// 执行函数
		c.Next()
		// 中间件执行完后续的一些事情
		status := c.Writer.Status()
		fmt.Println("中间件执行完毕", status)
		t2 := time.Since(t)
		fmt.Println("time:", t2)
	}
}

// consle控制台输出结果:
中间件开始执行了 
request: 我是中间件 
中间件执行完毕 200
time: 475.3µs
[GIN] 2022/07/16 - 11:52:59 | 200 |       994.3µs |             ::1 | GET      "/middle"

常用中间件推荐

三、Gin与net/http

实际上,Go的net/http包基本上提供了从服务请求到服务响应的全套服务

  • 原生ntp/http代码如下:
package main

import (
	"fmt"
	"net/http"
)

// 原生ntp/http请求
func main() {
    // 查看路由注册源码,可以发现原生路由注册非常简单
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("Hello World"))
	})
    // 服务监听及响应
    //【源码重点】建立socket,取到已经注册到的路由, 将正确的响应信息从handler中取出来返回给客户端
	if err := http.ListenAndServe(":8080", nil); err != nil {
		fmt.Println("start http server fail:", err)
	}
}

从net/http源码学习,服务监听与响应的大致流程如下:

在这里插入图片描述

  1. ln, err := net.Listen(“tcp”, addr) 做了初试化了socket、bind、listen的操作。
  2. rw, err := l.Accept() 进行accept,等待客户端进行连接。
  3. go c.serve(connCtx) 启动新的goroutine来处理本次请求。同时主goroutine继续等待客户端连接,进行高并发操作。
  4. h, _ := mux.Handler® 获取注册的路由,然后拿到这个路由的handler,然后将处理结果返回给客户端。

既然 net/http基本上提供了全套的服务,那为什么还需要类似于gin的web框架

从net/http的路由匹配规则来看:net/http的路由匹配根本就不符合 RESTful 的规则,遇到复杂一点的需求时,这个简单的路由匹配规则简直就是噩梦。

// Find a handler on a handler map given a path string.
// Most-specific (longest) pattern wins.
func (mux *ServeMux) match(path string) (h Handler, pattern string) {
	// Check for exact match first.
	v, ok := mux.m[path]
	if ok {
		return v.h, v.pattern
	}

	// Check for longest valid match.  mux.es contains all patterns
	// that end in / sorted from longest to shortest.
	for _, e := range mux.es {
		if strings.HasPrefix(path, e.pattern) {
			return e.h, e.pattern
		}
	}
	return nil, ""
}

所以基本所有的go web框架干的最主要的一件事情就是重写net/http的route。

甚至可以直接说 gin就是一个 httprouter 也不过分,当然gin也提供了其他比较主要的功能。

综上,net/http基本已经提供http服务的大部分功能,那些号称贼快的go框架,基本上都是提供一些功能,让我们能够更好的处理客户端发来的请求。

  • 自定义路由示例【重写route简易版】
package main

import (
	"fmt"
	"net/http"
)

// MyMux 自定义简易路由
type MyMux struct {
}
// 重写route
func (p *MyMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	if r.URL.Path == "/" {
		sayhelloName(w, r)
		return
	}
	http.NotFound(w, r)
	return
}

func sayhelloName(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Hello myroute!")
}

func main() {
	mux := &MyMux{}
	http.ListenAndServe(":9090", mux)
}

四、Gin路由原理

前面说过,Gin框架实际上就是一个route路由重写框架,那么Gin是如果引入路由生成路由树、进行路由查找的?

1、路由引入

gin启动server服务【router.run()】的底层依然是 http.ListenAndServe(),所以 gin 建立 socket 的过程,accept 客户端请求的过程与 net/http 没有差别,会同样重复上面的过程。唯一有差别的位置就是在于获取 ServeHTTP 的位置。

在这里插入图片描述

  • http请求路由引入源码流程
//由于 Handler 是 interface 类型,其真正的类型是 gin.Engine,根据 interace 的动态转发特性,最终会跳转到 gin.Engine.ServeHTTP 函数中来
// ServeHTTP conforms to the http.Handler interface.
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	c := engine.pool.Get().(*Context)      //从 sync.pool 里面拿去一块内存
	c.writermem.reset(w)                   //对这块内存做初始化工作,防止数据污染
	c.Request = req
	c.reset()
	engine.handleHTTPRequest(c)            //处理请求 handleHTTPRequest
	engine.pool.Put(c)                     //请求处理完成后,把这块内存归还到 sync.pool 中
}

2、路由注册

gin 框架中采用的路由库是基于httprouter做的,httprouter会将所有路由规则构造一颗前缀树。

其实,gin注册路由的实现很简单,不同的 方法就是一棵路由树,所以当 gin 注册路由的时候,会根据不同的 Method 分别注册不同的路由树。 如:

GET /user/{userID} HTTP/1.1
POST /user/{userID} HTTP/1.1
PUT /user/{userID} HTTP/1.1
DELETE /user/{userID} HTTP/1.1

  • 路由注册流程【生成路由树的源码流程】
func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
    // ... ...
	root := engine.trees.get(method)  //拿到一个 method 方法时,去 trees slice 中遍历
	if root == nil {   //如果没有找到,则重新创建一颗新的方法树出来, 然后将 URL对应的 handler 添加到这个路由 树上
		root = new(node)
		root.fullPath = "/"
		engine.trees = append(engine.trees, methodTree{method: method, root: root})
	}
	root.addRoute(path, handlers) //如果 trees slice 存在这个 method, 则这个URL对应的 handler 直接添加到找到的路由树上
	// ... ...
}
  • 路由树
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
}

当然最简单最粗暴的就是每个字符串占用一个树的叶子节点,不过这种设计会带来的问题:占用内存会升高,实际上Gin采用了共用前缀方式构建树。如: abc, abd, af 都是用共同的前缀的,如果能共用前缀的话,可以省内存空间。

3、路由查找

当 gin 收到客户端的请求时,第一件事就是去路由树里面去匹配对应的 URL,找到相关的路由,拿到相关的处理函数。其实这个过程就是 handleHTTPRequest 要干的事情。

  • 路由查找源码流程
func (engine *Engine) handleHTTPRequest(c *Context) {
    // ... ...
	// Find root of the tree for the given HTTP method
	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, c.skippedNodes, 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, c.skippedNodes, unescape); value.handlers != nil {
				c.handlers = engine.allNoMethod
				serveError(c, http.StatusMethodNotAllowed, default405Body)
				return
			}
		}
	}
	c.handlers = engine.allNoRoute
	serveError(c, http.StatusNotFound, default404Body)
}
  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

进击的程序猿~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值