gin框架是基于Golang的一个web框架,由于比较简单容易上手的开源框架
github地址:https://github.com/gin-gonic/gin
中文文档:https://gin-gonic.com/zh-cn/
1.特性
快速
基于 Radix 树的路由,小内存占用。没有反射。可预测的 API 性能。
支持中间件
传入的 HTTP 请求可以由一系列中间件和最终操作来处理。 例如:Logger,Authorization,GZIP,最终操作 DB。
Crash 处理
Gin 可以 catch 一个发生在 HTTP 请求中的 panic 并 recover 它。这样,你的服务器将始终可用。例如,你可以向 Sentry 报告这个 panic!
JSON 验证
Gin 可以解析并验证请求的 JSON,例如检查所需值的存在。
路由组
更好地组织路由。是否需要授权,不同的 API 版本…… 此外,这些组可以无限制地嵌套而不会降低性能。
错误管理
Gin 提供了一种方便的方法来收集 HTTP 请求期间发生的所有错误。最终,中间件可以将它们写入日志文件,数据库并通过网络发送。
内置渲染
Gin 为 JSON,XML 和 HTML 渲染提供了易于使用的 API。
2.具体使用
初始化与路由设置
router := gin.New() //new方法需要手动加载 Recovery 和 Logger 中间件router = gin.Default() //Default 使用 Logger 和 Recovery 中间件router.Use(middleware...) //注册全局中间件userGroup := router.Group("/user")userGroup.Use(middleware...) //注册路由组中间件userGroup.Get("/detail",userController.Detail)//http://localhost:8088/user/detail
创建一个get的API接口
func main() {r := gin.Default()r.GET("/testApi/print", func(c *gin.Context) {c.JSON(http.StatusOK, gin.H{"data": "hello world!",})})r.Run() // 监听并在 0.0.0.0:8080 上启动服务}
获取http请求的参数
c := gin.ContextuserName := c.Query("userName")//获取get参数form := c.PostForm("userName")//获取post参数file,header,err := c.Request.FormFile("file")//以io流的形式获取文件,可以不用保存到本地err = c.Bind(formStruct)//获取表单,推荐;formStruct是你想要的数据的结构体type formStruct struct{UserName string `json:"userName"`PassWord string `json:"passWord"`}
设置结束函数
gin.Context.Next()与gin.Context.Abort()通常放在函数的末尾,分别代表加载下一个和结束
//gin.Context.Next()func (c *Context) Next() {c.index++for c.index < int8(len(c.handlers)) {c.handlers[c.index](c)c.index++}}//gin.Context.Abort()const abortIndex int8 = math.MaxInt8 / 2func (c *Context) Abort() {c.index = abortIndex}
关闭http服务
func main() {router := gin.Default()router.GET("/", func(c *gin.Context) {time.Sleep(5 * time.Second)c.String(http.StatusOK, "Welcome Gin Server")})srv := &http.Server{Addr: ":8080",Handler: router,}go func() {// 服务连接if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {log.Fatalf("listen: %s\n", err)}}()// 等待中断信号以优雅地关闭服务器(设置 5 秒的超时时间)quit := make(chan os.Signal)signal.Notify(quit, os.Interrupt)<-quitlog.Println("Shutdown Server ...")ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)defer cancel()if err := srv.Shutdown(ctx); err != nil {log.Fatal("Server Shutdown:", err)}log.Println("Server exiting")}
3.底层代码分析
Engine 容器对象,整个框架的基础
Engine.trees 负责存储路由和handle方法的映射,采用类似字典树的结构,go的每种方法(GET,POST,DELETE等)都有自己单独的树
Engine.RouterGroup,其中的Handlers存储着所有中间件
Context上下文对象,负责处理请求和回应,其中的handlers是存储处理请求时中间件和处理方法的
开启http处理及路由匹配
可以看出底层还是使用了go自带的http包来进行处理,通过实现ServeHTTP方法来实现gin自己的http处理
go自带的处理函数虽然已经实现了基本功能,但是路由匹配功能过于简单(处理http请求是单独开启gorouti)
func (engine *Engine) Run(addr ...string) (err error) {//省略部分代码...address := resolveAddress(addr)debugPrint("Listening and serving HTTP on %s\n", address)err = http.ListenAndServe(address, engine)return}
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {c := engine.pool.Get().(*Context)//pool是一个sync.pool结构// Get对象后做初始化c.writermem.reset(w)c.Request = reqc.reset()//重置所有参数engine.handleHTTPRequest(c)engine.pool.Put(c)}
// gin.gofunc (engine *Engine) handleHTTPRequest(c *Context) {// ...// 根据请求方法找到对应的路由树t := engine.treesfor i, tl := 0, len(t); i < tl; i++ {if t[i].method != httpMethod {continue}root := t[i].root// Find route in treevalue := root.getValue(rPath, c.params, c.skippedNodes, unescape)if value.params != nil {c.Params = *value.params}if value.handlers != nil {c.handlers = value.handlersc.fullPath = value.fullPathc.Next() // 执行函数链条c.writermem.WriteHeaderNow()return}//...}// ...c.handlers = engine.allNoRouteserveError(c, http.StatusNotFound, default404Body)}
// tree.gotype nodeValue struct {handlers HandlersChainparams *Params //[]Param key->valuetsr boolfullPath string}func (n *node) getValue(path string, params *Params, skippedNodes *[]skippedNode, unescape bool) (value nodeValue) {var globalParamsCount int16walk: // Outer loop for walking the treefor {prefix := n.pathif len(path) > len(prefix) {if path[:len(prefix)] == prefix {path = path[len(prefix):]// Try all the non-wildcard children first by matching the indicesidxc := path[0]for i, c := range []byte(n.indices) {if c == idxc {// strings.HasPrefix(n.children[len(n.children)-1].path, ":") == n.wildChildif n.wildChild {index := len(*skippedNodes)*skippedNodes = (*skippedNodes)[:index+1](*skippedNodes)[index] = skippedNode{path: prefix + path,node: &node{path: n.path,wildChild: n.wildChild,nType: n.nType,priority: n.priority,children: n.children,handlers: n.handlers,fullPath: n.fullPath,},paramsCount: globalParamsCount,}}n = n.children[i]continue walk}}if !n.wildChild {// If the path at the end of the loop is not equal to '/' and the current node has no child nodes// the current node needs to roll back to last vaild skippedNodeif path != "/" {for l := len(*skippedNodes); l > 0; {skippedNode := (*skippedNodes)[l-1]*skippedNodes = (*skippedNodes)[:l-1]if strings.HasSuffix(skippedNode.path, path) {path = skippedNode.pathn = skippedNode.nodeif value.params != nil {*value.params = (*value.params)[:skippedNode.paramsCount]}globalParamsCount = skippedNode.paramsCountcontinue walk}}}// Nothing found.// We can recommend to redirect to the same URL without a// trailing slash if a leaf exists for that path.value.tsr = path == "/" && n.handlers != nilreturn}// Handle wildcard child, which is always at the end of the arrayn = n.children[len(n.children)-1]globalParamsCount++switch n.nType {case param:// fix truncate the parameter// tree_test.go line: 204// Find param end (either '/' or path end)end := 0for end < len(path) && path[end] != '/' {end++}// Save param valueif params != nil && cap(*params) > 0 {if value.params == nil {value.params = params}// Expand slice within preallocated capacityi := len(*value.params)*value.params = (*value.params)[:i+1]val := path[:end]if unescape {if v, err := url.QueryUnescape(val); err == nil {val = v}}(*value.params)[i] = Param{Key: n.path[1:],Value: val,}}// we need to go deeper!if end < len(path) {if len(n.children) > 0 {path = path[end:]n = n.children[0]continue walk}// ... but we can'tvalue.tsr = len(path) == end+1return}if value.handlers = n.handlers; value.handlers != nil {value.fullPath = n.fullPathreturn}if len(n.children) == 1 {// No handle found. Check if a handle for this path + a// trailing slash exists for TSR recommendationn = n.children[0]value.tsr = n.path == "/" && n.handlers != nil}returncase catchAll:// Save param valueif params != nil {if value.params == nil {value.params = params}// Expand slice within preallocated capacityi := len(*value.params)*value.params = (*value.params)[:i+1]val := pathif unescape {if v, err := url.QueryUnescape(path); err == nil {val = v}}(*value.params)[i] = Param{Key: n.path[2:],Value: val,}}value.handlers = n.handlersvalue.fullPath = n.fullPathreturndefault:panic("invalid node type")}}}if path == prefix {// If the current path does not equal '/' and the node does not have a registered handle and the most recently matched node has a child node// the current node needs to roll back to last vaild skippedNodeif n.handlers == nil && path != "/" {for l := len(*skippedNodes); l > 0; {skippedNode := (*skippedNodes)[l-1]*skippedNodes = (*skippedNodes)[:l-1]if strings.HasSuffix(skippedNode.path, path) {path = skippedNode.pathn = skippedNode.nodeif value.params != nil {*value.params = (*value.params)[:skippedNode.paramsCount]}globalParamsCount = skippedNode.paramsCountcontinue walk}}// n = latestNode.children[len(latestNode.children)-1]}// We should have reached the node containing the handle.// Check if this node has a handle registered.if value.handlers = n.handlers; value.handlers != nil {value.fullPath = n.fullPathreturn}// If there is no handle for this route, but this route has a// wildcard child, there must be a handle for this path with an// additional trailing slashif path == "/" && n.wildChild && n.nType != root {value.tsr = truereturn}// No handle found. Check if a handle for this path + a// trailing slash exists for trailing slash recommendationfor i, c := range []byte(n.indices) {if c == '/' {n = n.children[i]value.tsr = (len(n.path) == 1 && n.handlers != nil) ||(n.nType == catchAll && n.children[0].handlers != nil)return}}return}// Nothing found. We can recommend to redirect to the same URL with an// extra trailing slash if a leaf exists for that pathvalue.tsr = path == "/" ||(len(prefix) == len(path)+1 && prefix[len(path)] == '/' &&path == prefix[:len(prefix)-1] && n.handlers != nil)// roll back to last valid skippedNodeif !value.tsr && path != "/" {for l := len(*skippedNodes); l > 0; {skippedNode := (*skippedNodes)[l-1]*skippedNodes = (*skippedNodes)[:l-1]if strings.HasSuffix(skippedNode.path, path) {path = skippedNode.pathn = skippedNode.nodeif value.params != nil {*value.params = (*value.params)[:skippedNode.paramsCount]}globalParamsCount = skippedNode.paramsCountcontinue walk}}}return}}
总体来说,就是遍历整个前缀树,直到找到有handler方法的节点
// Next should be used only inside middleware.// It executes the pending handlers in the chain inside the calling handler.func (c *Context) Next() {c.index++for c.index < int8(len(c.handlers)) {c.handlers[c.index](c)c.index++}}
注意next方法要在中间件中使用,类似嵌套关系,使用next()代表执行完next()所在行之前的方法后就先加载链路后面的内容,否则就先执行本节点后面的内容
路由注册
在我们使用GET,POST等方法后,都会调用handle方法,只不过httpMethod对应的不同
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) {//省略判断路由格式代码...//trees是一个methodTree切片,包含method和root,root是一个*node,method是GET,POST等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...}
type node struct {// 当前节点路径path string// 和children字段对应, 保存的是分裂的分支的第一个字符// 例如search和support, 那么s节点的indices对应的"eu"// 代表有两个分支, 分支的首字母分别是e和uindices string// 儿子节点children []*node// 处理函数链条(切片)handlers HandlersChain// 优先级,子节点、子子节点等注册的handler数量priority uint32// 节点类型,包括static, root, param, catchAll// static: 静态节点(默认),比如上面的s,earch等节点// root: 树的根节点// catchAll: 有*匹配的节点// param: 参数节点nType nodeType// 路径上最大参数个数maxParams uint8// 节点是否是参数节点,比如上面的:postwildChild bool// 完整路径fullPath string}
不论是Group()还是Handle()都使用了calculateAbsolutePath和combineHandlers这两个方法,保存在RouterGroup这个指针对象中
//计算路由func (group *RouterGroup) calculateAbsolutePath(relativePath string) string {return joinPaths(group.basePath, relativePath)}func joinPaths(absolutePath, relativePath string) string {if relativePath == "" {return absolutePath}finalPath := path.Join(absolutePath, relativePath)if lastChar(relativePath) == '/' && lastChar(finalPath) != '/' {return finalPath + "/"}return finalPath}//方法拼接func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {finalSize := len(group.Handlers) + len(handlers)if finalSize >= int(abortIndex) {panic("too many handlers")}mergedHandlers := make(HandlersChain, finalSize)copy(mergedHandlers, group.Handlers)copy(mergedHandlers[len(group.Handlers):], handlers)return mergedHandlers}
// tree.go// addRoute 将具有给定句柄的节点添加到路径中。// 不是并发安全的func (n *node) addRoute(path string, handlers HandlersChain) {fullPath := pathn.priority++// 空树就直接插入当前节点if len(n.path) == 0 && len(n.children) == 0 {n.insertChild(path, fullPath, handlers)n.nType = rootreturn}parentFullPathIndex := 0walk:for {// 找到最长的通用前缀// 这也意味着公共前缀不包含“:”"或“*” /// 因为现有键不能包含这些字符。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, //子节点优先级-1fullPath: n.fullPath,}n.children = []*node{&child}// []byte for proper unicode char conversion, see #65n.indices = bytesconv.BytesToString([]byte{n.path[i]})n.path = path[:i]n.handlers = niln.wildChild = falsen.fullPath = fullPath[:parentFullPathIndex+i]}// 将新来的节点插入新的parent节点作为子节点if i < len(path) {path = path[i:]// 取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 != '*' && n.nType != catchAll {// []byte for proper unicode char conversion, see #65// 注意这里是直接拼接第一个字符到n.indicesn.indices += bytesconv.BytesToString([]byte{c})child := &node{fullPath: fullPath,}// 追加子节点n.children = append(n.children, child)n.incrementChildPrio(len(n.indices) - 1)n = child}else if n.wildChild {// 如果是参数节点n = n.children[len(n.children)-1]n.priority++// 检查通配符是否匹配if len(path) >= len(n.path) && n.path == path[:len(n.path)] &&// Adding a child to a catchAll is not possiblen.nType != catchAll &&// 检查更长的通配符, 例如 :name and :names(len(n.path) >= len(path) || path[len(n.path)] == '/') {continue walk}// Wildcard conflictpathSeg := pathif n.nType != catchAll {pathSeg = strings.SplitN(pathSeg, "/", 2)[0]}prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.pathpanic("'" + pathSeg +"' in new path '" + fullPath +"' conflicts with existing wildcard '" + n.path +"' in existing prefix '" + prefix +"'")}n.insertChild(path, fullPath, handlers)return}// 已经注册过的节点if n.handlers != nil {panic("handlers are already registered for path '" + fullPath + "'")}n.handlers = handlersreturn}}
// tree.gofunc (n *node) insertChild(numParams uint8, path string, fullPath string, handlers HandlersChain) {// 找到所有的参数for numParams > 0 {// 第一个通配符之前的前缀wildcard, i, valid := findWildcard(path)if i < 0 { // 没有发现通配符break}// 通配符的名称必须不包含':' 和 '*'if !valid {panic("only one wildcard per path segment is allowed, has: '" +wildcard + "' in path '" + fullPath + "'")}// 检查通配符是否有名称if len(wildcard) < 2 {panic("wildcards must be named with a non-empty name in path '" + fullPath + "'")}// 检查这个节点是否有已经存在的子节点// 如果我们在这里插入通配符,这些子节点将无法访问if len(n.children) > 0 {panic("wildcard segment '" + wildcard +"' conflicts with existing children in path '" + fullPath + "'")}if wildcard[0] == ':' { // paramif i > 0 {// 在当前通配符之前插入前缀n.path = path[:i]path = path[i:]}n.wildChild = truechild := &node{nType: param,path: wildcard,maxParams: numParams,fullPath: fullPath,}n.children = []*node{child}n = childn.priority++numParams--// 如果路径没有以通配符结束// 那么将有另一个以'/'开始的非通配符子路径。if len(wildcard) < len(path) {path = path[len(wildcard):]child := &node{maxParams: numParams,priority: 1,fullPath: fullPath,}n.children = []*node{child}n = child // 继续下一轮循环continue}// 否则我们就完成了。将处理函数插入新叶子中n.handlers = handlersreturn}// catchAllif i+len(wildcard) != len(path) || numParams > 1 {panic("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'")}if len(n.path) > 0 && n.path[len(n.path)-1] == '/' {panic("catch-all conflicts with existing handle for the path segment root in path '" + fullPath + "'")}// currently fixed width 1 for '/'i--if path[i] != '/' {panic("no / before catch-all in path '" + fullPath + "'")}n.path = path[:i]// 第一个节点:路径为空的catchAll节点child := &node{wildChild: true,nType: catchAll,maxParams: 1,fullPath: fullPath,}// 更新父节点的maxParamsif n.maxParams < 1 {n.maxParams = 1}n.children = []*node{child}n.indices = string('/')n = childn.priority++// 第二个节点:保存变量的节点child = &node{path: path[i:],nType: catchAll,maxParams: 1,handlers: handlers,priority: 1,fullPath: fullPath,}n.children = []*node{child}return}// 如果没有找到通配符,只需插入路径和句柄n.path = pathn.handlers = handlersn.fullPath = fullPath}
注册中间件
// Use adds middleware to the group, see example code in GitHub.func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {group.Handlers = append(group.Handlers, middleware...)return group.returnObj()}type RouterGroup struct {Handlers HandlersChain //type HandlersChain []HandlerFuncbasePath stringengine *Engineroot bool}
之前提到gin.Default()和gin.New()的不同在于是否需要自己加载log和reovery中间件
func Default() *Engine {debugPrintWARNINGDefault()engine := New()engine.Use(Logger(), Recovery()) // 默认注册的两个中间件return engine}
参考
https://baijiahao.baidu.com/s?id=1716852044736696668&wfr=spider&for=pc
https://blog.csdn.net/luo1324574369/article/details/108310032
https://blog.csdn.net/weixin_45961841/article/details/113722686
https://blog.csdn.net/qq_37767455/article/details/104712028
