相关链接:
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"
常用中间件推荐:
- RestGate - REST API端点的安全身份验证
- staticbin - 用于从二进制数据提供静态文件的中间件/处理程序
- gin-cors - CORS杜松子酒的官方中间件
- gin-csrf - CSRF保护
- gin-health - 通过gocraft/health报告的中间件
- gin-merry - 带有上下文的漂亮 打印 错误的中间件
- gin-revision - 用于Gin框架的修订中间件
- gin-jwt - 用于Gin框架的JWT中间件
- gin-sessions - 基于mongodb和mysql的会话中间件
- gin-location - 用于公开服务器的主机名和方案的中间件
- gin-nice-recovery - 紧急恢复中间件,可让您构建更好的用户体验
- gin-limit - 限制同时请求;可以帮助增加交通流量
- gin-limit-by-key - 一种内存中的中间件,用于通过自定义键和速率限制访问速率。
- ez-gin-template - gin简单模板包装
- gin-hydra - gin中间件Hydra
- gin-glog - 旨在替代Gin的默认日志
- gin-gomonitor - 用于通过Go-Monitor公开指标
- gin-oauth2 - 用于OAuth2
- static gin框架的替代静态资产处理程序。
- xss-mw - XssMw是一种中间件,旨在从用户提交的输入中“自动删除XSS”
- gin-helmet - 简单的安全中间件集合。
- gin-jwt-session - 提供JWT / Session / Flash的中间件,易于使用,同时还提供必要的调整选项。也提供样品。
- gin-template - 用于gin框架的html / template易于使用。
- gin-redis-ip-limiter - 基于IP地址的请求限制器。它可以与redis和滑动窗口机制一起使用。
- gin-method-override - _method受Ruby的同名机架启发而被POST形式参数覆盖的方法
- gin-access-limit - limit-通过指定允许的源CIDR表示法的访问控制中间件。
- gin-session - 用于Gin的Session中间件
- gin-stats - 轻量级和有用的请求指标中间件
- gin-statsd - 向statsd守护进程报告的Gin中间件
- gin-health-check - check-用于Gin的健康检查中间件
- gin-session-middleware - 一个有效,安全且易于使用的Go Session库。
- ginception - 漂亮的例外页面
- gin-inspector - 用于调查http请求的Gin中间件。
- gin-dump - Gin中间件/处理程序,用于转储请求和响应的标头/正文。对调试应用程序非常有帮助。
- go-gin-prometheus - Gin Prometheus metrics exporter
- ginprom - Gin的Prometheus指标导出器
- gin-go-metrics - Gin middleware to gather and store metrics using rcrowley/go-metrics
- ginrpc - Gin 中间件/处理器自动绑定工具。通过像beego这样的注释路线来支持对象注册
三、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源码学习,服务监听与响应的大致流程如下:
- ln, err := net.Listen(“tcp”, addr) 做了初试化了socket、bind、listen的操作。
- rw, err := l.Accept() 进行accept,等待客户端进行连接。
- go c.serve(connCtx) 启动新的goroutine来处理本次请求。同时主goroutine继续等待客户端连接,进行高并发操作。
- 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)
}