likes
comments
collection
share

就决定是你啦,gin!

作者站长头像
站长
· 阅读数 9

前言

我有一个朋友,他叫李晓得,入行多年php,业务代码得心应手。左手laravel,右手swoole,一杯茶,一根烟,一行代码敲一天。终于有一天他做了一个决定:“我要转做golang!”隔壁B哥见状面露鄙夷之色:“go只是个玩具,php才是世界上最好的语言。”李晓得当即表示:“我不管,我就要折腾...”随后收拾起包袱,踏上了golang的转型之路...

开局一个web,经验全靠敲

在经过李晓得的充分准备之后,他开始了第一个单体web应用选择,他参考了github的:

框架名称github-star诞生日期备注
gin68k+2014年轻量级,高性能,流行度高
beego29k+2012年国人开发的web框架,老牌,集成度和成熟度高
fiber25k+2020年基于fasthttp实现(没错,就是那个可以比net/http快10倍的http实现),后起之秀
echo25k+2015年高性能、极简框架
iris23k+2016年提供完整的MVC

web框架的使用都是大同小异,没有过多的犹豫,李晓得选择了gin,因为gin够轻量,是个不错的学习框架。

不一会,按照gin官网照葫芦画瓢第一个"hello world"就出来了,easy!

package main

import (
    "context"
    "github.com/gin-gonic/gin"
    "log"
    "net/http"
    "os"
    "os/signal"
    "time"
)

func main() {
    //设置路由
    router := gin.Default()
    router.Use(gin.Recovery())
    router.Use(corsMiddleware())
    router.GET("/", func(c *gin.Context) {
        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)
    <-quit
    log.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")
}

// corsMiddleware cors中间件
func corsMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Header("Access-Control-Allow-Origin", "*")
        c.Header("Access-Control-Allow-Headers", "Authorization, Content-Length, X-CSRF-Token, Token,session,X_Requested_With,Accept, Origin, Host, Connection, Accept-Encoding, Accept-Language,DNT, X-CustomHeader, Keep-Alive, User-Agent, X-Requested-With, If-Modified-Since, Cache-Control, Content-Type, Pragma")
        c.Header("Access-Control-Allow-Methods", "POST,GET,OPTIONS,PUT,DELETE")
        c.Header("Access-Control-Allow-Credentials", "true")

        method := c.Request.Method
        if method == "OPTIONS" {
            c.AbortWithStatus(http.StatusNoContent)
        }
        c.Next()
    }
}

运行访问一下,很顺利,看下响应时间:"0s",很合理吧。

[GIN-debug] GET    /                         --> main.main.func1 (5 handlers)
[GIN] xxxx/xx/xx - xx:xx:xx | 200 |            0s |       127.0.0.1 | GET      "/"

这个请求都经历了些啥?

首先,accept客户端的发起的连接,新建c.serve协程处理该请求。

    for {
        rw, err := l.Accept()
        if err != nil {
            //错误逻辑处理
            return err
        }
        connCtx := ctx
        //...
        c := srv.newConn(rw)
        c.setState(c.rwc, StateNew, runHooks) // before Serve can return
        go c.serve(connCtx)
    }

进入serve中的ServeHTTP方法处理

// Serve a new connection.
func (c *conn) serve(ctx context.Context) {
    //...
    serverHandler{c.server}.ServeHTTP(w, w.req)
}

由于go语言的接口都是隐式继承的,所以调用的gin.Engine下的ServeHTTP方法。gin使用的是sync.Pool管理对象的思想对协程使用非常友好,究其原因是对象池拥有重用性,重用对象避免请求过多时内存的频繁申请,同时大大降低了垃圾回收所带来的消耗。有效避免其多个协程后,每个请求协程所带来的gc开销过大导致协程优势丧失。

// ServeHTTP conforms to the http.Handler interface.
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)
}

让我们看看真正开始处理请求handleHTTPRequest()都干了些啥。

func (engine *Engine) handleHTTPRequest(c *Context) {
    // 略
    // 这里gin用的是Radix Tree(基数树)的方式保存路由信息,这里不展开讲,有兴趣可以自行了解
    t := engine.trees
    for i, tl := 0, len(t); i < tl; i++ {
        if t[i].method != httpMethod {
            continue
        }
        root := t[i].root
        // 在基数树查找到路由结果
        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() // 处理函数链的调用 包括middleware和需要处理业务逻辑的函数
            c.writermem.WriteHeaderNow()
            return
        }
        if httpMethod != http.MethodConnect && rPath != "/" {
            if value.tsr && engine.RedirectTrailingSlash {
                redirectTrailingSlash(c)
                return
            }
            if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) {
                return
            }
        }
        break
    }
    // 略
}

不是吧,这年头没点算法功底都没法看代码啊,为啥要用基数树啊。那是因为基数树拥有公共前缀字符串的特点,查询高效,空间利用率高,拓展性强!不懂,很好,那就把想懂的想法吞到肚子里。

至此,整个请求的处理链路已经很清晰了。

就决定是你啦,gin!

好的,简简单单拿下今日首胜~李晓得感觉自己行了!

本故事纯属虚构,怎么可能是真事,不会是真事吧...

转载自:https://juejin.cn/post/7225454068406353979
评论
请登录