Gin is a HTTP web framework written in Go (Golang). It features a Martini-like API, but with performance up to 40 times faster than Martini. If you need smashing performance, get yourself some Gin.
QuickStart
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "pong",
})
})
r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
}
这里我们使用 gin.Default() 来初始化一个 Engine
Engine 是框架的实例,它包含muxer、中间件和配置设置
func Default() *Engine {
debugPrintWARNINGDefault()
engine := New()
engine.Use(Logger(), Recovery())
return engine
}
这里我们可以看出 gin.Default() 实际上是调用了 gin.New() 初始化一个原生的 Engine,然后调用 engine.Use() 绑定两个全局的中间件,Logger 用于打印日志,Recovery 用于防止 panic
RouterGroup
func main() {
r := gin.New()
v1 := r.Group("/v1")
v1.GET("/index", func(c *gin.Context) {
c.String(http.StatusOK, "hello world")
})
user := v1.Group("/admin")
user.GET("/hello", func(c *gin.Context) {
c.String(http.StatusOK, "hello admin")
})
r.Run()
}
通过创建路由组来返回一个路由组对象,使用路由组添加路由,可以节省填写相同路径前缀和中间件的步骤,实际运行结果如下
$ curl --location --request GET http://localhost:8080/v1/index
hello world%
$ curl --location --request GET http://localhost:8080/v1/admin/hello
hello admin%
无论是 GET 或者 POST 方法,都是调用了 RouterGroup.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()
}
调用 Engine.addRouter() 将路由方法、路径、处理器切片存入路由树中
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
}
}
目前业界 Server 端 API 接口的设计方式一般是遵循 RESTful 风格的规范,同时 gin 注册路由的时候,会根据不同的 Method 分别注册不同的路由树
root := engine.trees.get(method) 得到方法的根节点,如果为空,则新建一个路由树
root.addRoute(path, handlers) 具有给定处理器的节点添加到路径中
生成路由树: gin 路由树是一棵前缀树
一个路径,比如/p/blog,它被/正斜杠划分为多段,如果我们把其中的一段给抽象为一个类似于节点,然后通过指针把这几段连接起来,形成一个前缀树,如下图所示
middleware
ginServer.Use() 注册中间件,那么当前服务的所有请求都会被该中间件拦截处理,结合路由分组,gin 支持全局中间件,局部中间件和单个路由的中间件
func main() {
r := gin.New()
r.GET("/index", func(c *gin.Context) {
c.String(http.StatusOK, "hello world")
})
// 绑定全局中间件,但是上面一部分函数不会使用
r.Use(gin.Logger())
v1 := r.Group("/v1")
// 绑定局部中间件
v1.Use(func(c *gin.Context) {
c.Set("version", "v1")
c.Next()
})
// 为单个路由绑定中间件
v1.GET("/admin", middleware1(), func(c *gin.Context) {
value, _ := c.Get("admin")
c.JSON(http.StatusOK, gin.H{
"name": value,
"msg": "hello",
})
log.Println("GET response json......")
})
r.Run(":8080")
}
func middleware1() gin.HandlerFunc {
return func(c *gin.Context) {
log.Println("middleware 1 start......")
if v, e := c.Get("version"); e == false || v != "v1" {
c.Abort()
}
c.Set("admin", "admin001")
c.Next()
log.Println("middleware 1 end......")
}
}
运行上面的程序,服务端会输出如下字符串
$ curl --location --request GET http://localhost:8080/v1/index
middleware 1 start......
GET response json......
middleware 1 end......
从上面的程序其实可以看出来,gin 的中间件和普通处理函数并没有区别,都是 type HandlerFunc func(*Context) 类型
但是,在中间件中,需要通过 Next 和 Abort 方法,控制 handler 执行顺序
//Next只能在中间件内部使用。
//它在调用处理程序内部执行链中的挂起处理程序。
func (c *Context) Next() {
c.index++
// 限制调用的嵌套层数
for c.index < int8(len(c.handlers)) {
c.handlers[c.index](c)
c.index++
}
}
const abortIndex int8 = math.MaxInt8 >> 1
func (c *Context) Abort() {
c.index = abortIndex
}
这样可以把中间件的调用,理解为 洋葱模型
这也就是为什么开始运行的 demo,在执行 middleware1() 中 log.Println("middleware 1 start......") 后,调用 c.NEXT() 执行 index++ 进入下一层,最后返回执行 log.Println("middleware 1 end......"),多嵌套几个中间件也是同样的道理
如果需要自己实现一个中间件的结构,
gin.Context
在中间件中,通过 gin.context 管理 HandlerFunc 的执行顺序,同时它还可以链式传递共享变量
gin.context的定义如下
type Context struct {
writermem responseWriter //相应处理
Request *http.Request //请求报文的抽象
Writer ResponseWriter //响应报文的抽象
Params Params //记录了url参数,是一组键值对
handlers HandlersChain //定义一个 HandlerFunc 数组
index int8 //定义一个HandlerFunc数组的索引。next() / abort() 会用到
fullPath string //请求地址
engine *Engine //gin 框架实例对象,gin.New()返回的就是一个 \*Engine 实例
params *Params //URL 参数的切片
skippedNodes *[]skippedNode //路由中配置的需要跳过的路径节点
mu sync.RWMutex //读写锁,用来保护Keys
Keys map[string]any //是专门针对每个请求的上下文的键/值对,Set()和Get()的数据就放在这里。
Errors errorMsgs //使用此上下文的所有处理程序或者中间件附带的错误列表
Accepted []string //自定义请求接收的内容类型格式
queryCache url.Values //底层数据类型是 map[string][]string,使用url.ParseQuery()从c.Request.URL.Query()缓存了参数查询结果
formCache url.Values //使用url.ParseQuery缓存的PostForm包含来自POST,PATCH或者PUT请求参数
sameSite http.SameSite //允许服务器定义cookie属性。用来防止 CSRF 攻击和用户追踪
}
对官方context的实现
func (c *Context) Deadline() (deadline time.Time, ok bool) {
// ContextWithFallback默认是false 一般都不会回退的。
if !c.engine.ContextWithFallback || c.Request == nil || c.Request.Context() == nil {
return
}
//如果允许回退,那么Context会直接回退为Request下的Context。
return c.Request.Context().Deadline()
}
// Value 会优先返回元数据中的Key,如果没有才会尝试去Request中的Context中查询。
func (c *Context) Value(key any) any {
//两种特殊情况
if key == 0 {
return c.Request //返回Request结构体
}
if key == ContextKey {
return c //返回Context本身
}
if keyAsString, ok := key.(string); ok {
if val, exists := c.Get(keyAsString); exists {
return val
}
}
if !c.engine.ContextWithFallback || c.Request == nil || c.Request.Context() == nil {
return nil
}
return c.Request.Context().Value(key)
}
在上下文中传递数据
Set 和 Get,为此上下文存储新的键值对。存储在Context中的Keys数据字段中,如果以前没有使用过,它会延迟初始化。
func (c *Context) Set(key string, value any) {
c.mu.Lock()
defer c.mu.Unlock()
if c.Keys == nil {
c.Keys = make(map[string]any)
}
c.Keys[key] = value
}
func (c *Context) Get(key string) (value any, exists bool) {
c.mu.RLock()
defer c.mu.RUnlock()
value, exists = c.Keys[key]
return
}
获取Param数据
func (c *Context) Param(key string) string {
return c.Params.ByName(key)
}
举一个例子
r.GET("/video/:id", func(context *gin.Context) {})
curl --location --request GET http://localhost:8080/video/1
id := c.Param("id") // id = 1
获取Query数据
// TODO
获取header数据
// TODO
获取body数据
// TODO
请求流转
请求是如何从 gin 流入 net/http, 最后又回到 gin 中
//Run将路由器连接到http.Server,并开始侦听和提供http请求。
//这是http.ListnAndServe(地址,路由器)的快捷方式
//注意:除非发生错误,否则此方法将无限期地阻止调用goroutine。
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
}
通过 http.ListenAndServe(),将 gin 自己实现的处理器传入,然后通过 net/http 建立连接
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
handler := sh.srv.Handle
...
handler.ServeHTTP(rw, req)
}
sh.srv.Handle 是一个接口,其真正的类型是 gin.Engine,根据 interace 的动态转发特性,最终会跳转到 gin.Engine.ServeHTTP 函数中
// ServeHTTP conforms to the http.Handler interface.
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// 从 sync.pool 里面取一块内存
c := engine.pool.Get().(*Context)
c.writermem.reset(w)
c.Request = req
c.reset()
// 处理请求
engine.handleHTTPRequest(c)
// 请求处理结束,归还内粗
engine.pool.Put(c)
}