Gin基本使用-原理篇
go mod
类似于我们Python的pip做依赖管理的,每个项目可能都会使用一些外部包,外步包有很多版本
go mod
就是帮助我们自动管理你们的包和版本号的- 如果没有go mod别人如何才能运行你们的代码
外部的包:其他人封装好的,实现特定功能的代码
常用命令:
go get github.com/gin-gonic/gin
// 将GitHub上,或者其他仓库里的代码下载到本地,直接导入就可以是用了go mod tidy
// 更新我们的依赖(第一次运行项目,执行这个命令他会将项目中所有的外部包一次性的全部下载到本地)
https://www.gl.sh.cn/2022/04/02/bao_guan_li_gong_ju_go_mod.html
Gin入门
1、介绍
- Gin是一个golang的微框架,封装比较优雅,API友好,源码注释比较明确,具有快速灵活,容错方便等特点
- 对于golang而言,web框架的依赖要远比Python,Java之类的要小。自身的 net/http 足够简单,性能也非常不错
- 借助框架开发,不仅可以省去很多常用的封装带来的时间,也有助于团队的编码风格和形成规范
2、安装
要安装Gin软件包,您需要安装Go并首先设置Go工作区。
1.首先需要安装Go(需要1.10+版本),然后可以使用下面的Go命令安装Gin。
go get -u github.com/gin-gonic/gin
2.将其导入您的代码中:
import "github.com/gin-gonic/gin"
3.(可选)导入net/http
。例如,如果使用常量,则需要这样做http.StatusOK
。
import "net/http"
3、hello word
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
// 1.创建 (实例化gin.Engine结构体对象)
r := gin.Default()
// 2.绑定路由规则,执行的函数
// gin.Context,封装了request和response
r.GET("/", func(c *gin.Context) { c.String(http.StatusOK, "hello World!") })
// 3.监听端口,默认在8080
// Run("里面不指定端口号默认为8080")
r.Run(":8000")
}
Gin工作流程
1、gin核心概念
- Engine 容器对象,整个框架的基础
- Engine.trees 负责存储路由和handle方法的映射,采用类似字典树的结构
- Engine.RouterGroup ,其中的Handlers存储着所有中间件
- Context 上下文对象,负责处理 请求和回应 ,其中的 handlers 是存储处理请求时中间件和处理方法的
2、 请求生命周期
图将就着看啊~~~~
Gin源码
https://www.gl.sh.cn/2022/04/02/gin_yuan_ma_jie_xi.html
1、先看 gin.Default()
Default()
跟New()
几乎一模一样, 就是调用了gin内置的Logger()
, Recovery()
中间件
// Default返回一个已经附加了Logger和Recovery中间件的Engine实例
func Default() *Engine {
debugPrintWARNINGDefault()
engine := New() // 默认实例
// 注册中间建,中间件的是一个函数,最终只要返回一个 type HandlerFunc func(*Context) 就可以
engine.Use(Logger(), Recovery()) // 默认注册的两个中间件
return engine
}
2、engine := New()
初始化
通过调用 gin.New()
方法来实例化 Engine容器 .
1.初始化了Engine
2.将RouterGroup
的Handlers
(数组)设置成nil
, basePath
设置成 /
3.为了使用方便, RouteGroup里面也有一个Engine指针, 这里将刚刚初始化的engine赋值给了RouterGroup的engine指针
4.为了防止频繁的context GC造成效率的降低, 在Engine里使用了sync.Pool
, 专门存储gin的 Context
func New() *Engine {
debugPrintWARNINGNew()
// Engine 容器对象,整个框架的基础
engine := &Engine{ // 初始化语句
// Handlers 全局中间件组在注册路由时使用
RouterGroup: RouterGroup{ // Engine.RouterGroup,其中的Handlers存储着所有中间件
Handlers: nil,
basePath: "/",
root: true,
},
// 树结构,保存路由和处理方法的映射
trees: make(methodTrees, 0, 9),
}
engine.RouterGroup.engine = engine
return engine
}
engine.Use()
注册中间件
gin框架中的中间件设计很巧妙,我们可以首先从我们最常用的 r := gin.Default()
的 Default 函数开始看
它内部构造一个新的 engine 之后就通过 Use()
函数注册了 Logger 中间件和 Recovery 中间件
Use()
就是gin的引入中间件的入口了.
仔细分析这个函数, 不难发现Use()
其实是在给RouteGroup引入中间件的.
具体是如何让中间件在RouteGroup上起到作用的, 等说到RouteGroup再具体说.
func Default() *Engine {
debugPrintWARNINGDefault()
engine := New()
engine.Use(Logger(), Recovery()) // 默认注册的两个中间件
return engine
}
gin.use()
调用 RouterGroup.Use()
往 RouterGroup.Handlers
写入记录
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
engine.RouterGroup.Use(middleware...)
engine.rebuild404Handlers() //注册404处理方法
engine.rebuild405Handlers() //注册405处理方法
return engine
}
// 其中`Handlers`字段就是一个数组,用来存储中间件
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
group.Handlers = append(group.Handlers, middleware...)
return group.returnObj()
}
组成一条处理函数链条 HandlersChain
- 也就是说,我们会将一个路由的中间件函数和处理函数结合到一起组成一条处理函数链条HandlersChain
- 而它本质上就是一个由HandlerFunc组成的切片
type HandlersChain []HandlerFunc
中间件的执行
其中 c.Next()
就是很关键的一步,它的代码很简单
从下面的代码可以看到,这里通过索引遍历 HandlersChain 链条
从而实现依次调用该路由的每一个函数(中间件或处理请求的函数)
我们可以在中间件函数中通过再次调用 c.Next()
实现嵌套调用(func1中调用func2;func2中调用func3)
func (c *Context) Next() {
c.index++
for c.index < int8(len(c.handlers)) {
c.handlers[c.index](c)
c.index++
}
}
r.GET()
注册路由
r.GET("/", func(c *gin.Context) {
c.String(http.StatusOK, "hello World!")
})
通过Get方法将路由和处理视图函数注册
func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle(http.MethodPost, 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()
}
addRoute构造路由树
这段代码就是利用method, path, 将handlers注册到engine的trees中.
注意这里为什么是HandlersChain呢, 可以简单说一下, 就是将中间件和处理函数都注册到method, path的tree中了.
// tree.go
// addRoute 将具有给定句柄的节点添加到路径中。
// 不是并发安全的
func (n *node) addRoute(path string, handlers HandlersChain) {
fullPath := path
n.priority++
numParams := countParams(path) // 数一下参数个数
// 空树就直接插入当前节点
if len(n.path) == 0 && len(n.children) == 0 {
n.insertChild(numParams, path, fullPath, handlers)
n.nType = root
return
}
parentFullPathIndex := 0
walk:
for {
// 更新当前节点的最大参数个数
if numParams > n.maxParams {
n.maxParams = numParams
}
// 找到最长的通用前缀
// 这也意味着公共前缀不包含“:”"或“*” /
// 因为现有键不能包含这些字符。
i := longestCommonPrefix(path, n.path)
// 分裂边缘(此处分裂的是当前树节点)
// 例如一开始path是search,新加入support,s是他们通用的最长前缀部分
// 那么会将s拿出来作为parent节点,增加earch和upport作为child节点
if i < len(n.path) {
child := node{path: n.path[i:], // 公共前缀后的部分作为子节点
wildChild: n.wildChild, indices: n.indices, children: n.children, handlers: n.handlers,
priority:
n.priority - 1, //子节点优先级-1
fullPath: n.fullPath,
}
// Update maxParams (max of all children)
for _, v := range child.children {
if v.maxParams > child.maxParams {
child.maxParams = v.maxParams
}
}
n.children = []*node{&child}
// []byte for proper unicode char conversion, see #65
n.indices = string([]byte{n.path[i]})
n.path = path[:i]
n.handlers = nil
n.wildChild = false
n.fullPath = fullPath[:parentFullPathIndex+i]
}
// 将新来的节点插入新的parent节点作为子节点
if i < len(path) {
path = path[i:]
if n.wildChild { // 如果是参数节点
parentFullPathIndex += len(n.path)
n = n.children[0]
n.priority++
// Update maxParams of the child node
if numParams > n.maxParams {
n.maxParams = numParams
}
numParams--
// 检查通配符是否匹配
if len(path) >= len(n.path) && n.path == path[:len(n.path)] {
// 检查更长的通配符, 例如 :name and :names
if len(n.path) >= len(path) || path[len(n.path)] == '/' {
continue walk
}
}
pathSeg := path
if n.nType != catchAll {
pathSeg = strings.SplitN(path, "/", 2)[0]
}
prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path
panic("'" + pathSeg + "' in new path '" + fullPath + "' conflicts with existing wildcard '" + n.path + "' in existing prefix '" + prefix + "'")
}
// 取path首字母,用来与indices做比较
c := path[0]
// 处理参数后加斜线情况
if n.nType == param && c == '/' && len(n.children) == 1 {
parentFullPathIndex += len(n.path)
n = n.children[0]
n.priority++
continue walk
}
// 检查路path下一个字节的子节点是否存在
// 比如s的子节点现在是earch和upport,indices为eu
// 如果新加一个路由为super,那么就是和upport有匹配的部分u,将继续分列现在的upport节点
for i, max := 0, len(n.indices); i < max; i++ {
if c == n.indices[i] {
parentFullPathIndex += len(n.path)
i = n.incrementChildPrio(i)
n = n.children[i]
continue walk
}
}
// 否则就插入
if c != ':' && c != '*' {
// []byte for proper unicode char conversion, see #65
// 注意这里是直接拼接第一个字符到n.indices
n.indices += string([]byte{c})
child := &node{maxParams: numParams,
fullPath:
fullPath,
}
// 追加子节点
n.children = append(n.children, child)
n.incrementChildPrio(len(n.indices) - 1)
n = child
}
n.insertChild(numParams, path, fullPath, handlers)
return
}
// 已经注册过的节点
if n.handlers != nil {
panic("handlers are already registered for path '" + fullPath + "'")
}
n.handlers = handlers
return
}
}
r.run()
启动服务
通过调用 net/http
来启动服务,由于 engine 实现了 ServeHTTP 方法
只需要直接传 engine 对象就可以完成初始化并启动
func (engine *Engine) Run(addr ...string) (err error) {
defer func() { debugPrintError(err) }()
address := resolveAddress(addr)
debugPrint("Listening and serving HTTP on %s\n", address)
// 在golang中,你要构建一个web服务,必然要用到http.ListenAndServe
// 第二个参数必须要有一个handler
err = http.ListenAndServe(address, engine) // gin使用net/http模块
return
}
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
//来自 net/http 定义的接口,只要实现了这个接口就可以作为处理请求的函数
type Handler interface{ ServeHTTP(ResponseWriter, *Request) }
//实现了ServeHTTP方法
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)
}