一个用 golang 开发的轻量restfull框架

先把项目地址给大家


初衷

golang的学习及工作使用中,会经常遇到提供http服务的场景,这时有两个选择:自己使用http原生包去做(适合简单的api);使用第三方框架;我本身喜欢“重复造车轮”,这样使用自己开发的框架时遇到问题也可以很快的解决,而且也可以根据自身的业务特点进行快速适配。

当前有好多web框架,性能和功能也都各有不同。抱着学习的心态去从头做了一个轻量级的restfull框架,cotton就在这样的背景下诞生了。

cotton意指“棉花”,我也希望这个框架是轻量好用的。

要支持的特性

  • 速度快
  • 支持 restfull 格式参数
  • 支持中间件
  • 支持分组
  • 自定义日志
  • 自定义 panic
  • 自定义 NotFound
  • 分组自定义 NotFound
  • 静态文件
  • 模板
  • post参数相关,及文件上传相关

可用的http框架

开发中遇到的问题

主要对标的是 httprouter

路由结构存储

最开始时使用全路径正则实现,虽然功能都已经实现,但性能和 httprouter 相差太多,做 Benchmark 时不是一个数量级

全路径正则实现

    /user/:id/:name      =>   regexp.MustCompile("/user/(:\w+)/(:\w+)")

Benchmark 结果

cotton-bench tonny$ go test -bench=.
GithubAPI Routes: 203
           cottonRouter:     93080 bytes
             HttpRouter:     35768 bytes
goos: darwin
goarch: amd64
pkg: cottonbench
cpu: Intel(R) Core(TM) i5-8279U CPU @ 2.40GHz
BenchmarkCottonRouterWithGithubAPI-8                1543            792932 ns/op  598146 B/op        8100 allocs/op
BenchmarkHttpRouterWithGithubAPI-8                 25250             46397 ns/op   20320 B/op         334 allocs/op

PASS
ok      cottonbench     10.259s

思考

从结果可以ns/opallocs/op看出每次单次内存分配和运算所用的时间比较多,细分析每次请求后此方案只能按顺序从所有的已经路由正则里去匹配,在大量路由面前性能会直线下降。

知道了问题出在哪里就知道怎么去优化了

性能优化之路

1. 内存逃逸

内存逃逸相关的概念这里就不多说了,就说下我使用的方案

1.1 路由Handle时使用的Context 使用了 sync.Pool

var ctxPool sync.Pool

func init() {
    ctxPool.New = func() interface{} {
        return &Context{}
    }
}

func newContext(w http.ResponseWriter, r *http.Request, router *Router) *Context {
    // use sync.Pool
    ctx := ctxPool.Get().(*Context)

    // reset all property
    ctx.Request = r
    ctx.Response = &resWriter{
        ResponseWriter: w,
        statusCode:     http.StatusOK,
    }
    ctx.router = router
    ctx.indexAbort = -1
    ctx.index = -1
    ctx.handlers = ctx.handlers[0:0]
    ctx.paramCache = nil
    ctx.queryCache = nil

    ctxPool.Put(ctx)
    return ctx
}

1.2 路由匹配到的restfull参数使用sync.Pool

var paramsPool sync.Pool
func init() {
    paramsPool.New = func() interface{} {
        return make(map[string]string)
    }
}

result.params = paramsPool.Get().(map[string]string)

paramsPool.Put(result.params)

2. 数据结构

仔细研读了httprouter的源码,发现它使用的是前缀树或字典树,一种节省存储但查询效率很高的数据结构,但其实现的算法有些深奥,自己决定使用自己的数据结构,我使用的是按路径分割存储,很直观的树存储。

/search/
/support/
/blog/:post/
/about-us/
/about-us/team/
/contact/

2.1 前缀树

Priority   Path             Handle
9          \                *<1>
3          ├s               nil
2          |├earch\         *<2>
1          |└upport\        *<3>
2          ├blog\           *<4>
1          |    └:post      nil
1          |         └\     *<5>
2          ├about-us\       *<6>
1          |        └team\  *<7>
1          └contact\        *<8>

2.2 直观树

Deep        Path                       Handle
0           /                           nil
1             |--search                 nil
2                 |--/                  *1
1             |--support                nil
2                 |--/                  *2
1             |--blog                   nil
2                 |--/                  *3
2                 |--:post              *4
1             |--about-us               nil
2                 |--/                  *5
2                 |--team               nil
3                     |--/              *6
1             |--contact                nil
2                 |--/                  *7

3. 最终效果

GithubAPI Routes: 205
   cottonRouter:     95352 bytes
     HttpRouter:     36016 bytes
goos: darwin
goarch: amd64
pkg: cottonbench
cpu: Intel(R) Core(TM) i5-8279U CPU @ 2.40GHz
BenchmarkHttpRouterWithGithubAPI-8                 39334             31510 ns/op           13856 B/op        169 allocs/op
BenchmarkCottonRouterWithGithubAPI-8               34222             35289 ns/op               0 B/op          0 allocs/op

PASS
ok      cottonbench     11.384s

可以看出和 httprouter的性能差不多,性能优化算是很成功的。Benchmark 的代码参考 GithubGitee

如何使用

package main

import (
    "encoding/json"
    "fmt"
    "os"
    "path/filepath"

    "net/http"

    "github.com/tonny-zhang/cotton"
)

func main() {

    r := cotton.NewRouter()

    // writer logger to file
    // f, e := os.OpenFile("1.log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0600)
    // fmt.Println(f, e)
    // r.Use(cotton.Logger(), cotton.LoggerWidthConf(cotton.LoggerConf{
    //  Writer: f,
    // }))

    // r.Use(cotton.Recover())
    r.Use(cotton.Logger())
    // r.Use(func(ctx *cotton.Context) {
    //  fmt.Println("first")
    //  ctx.Abort()
    // })
    r.Use(cotton.RecoverWithWriter(nil, func(ctx *cotton.Context, err interface{}) {
        strErr := ""
        switch err.(type) {
        case string:
            strErr = err.(string)
        case error:
            strErr = err.(error).Error()
        default:
            if b, err := json.Marshal(err); err == nil {
                strErr = string(b)
            }
        }
        ctx.String(http.StatusInternalServerError, "[500 error]"+strErr)
    }))

    dir, _ := os.Getwd()
    r.Group("/static/", cotton.LoggerWidthConf(cotton.LoggerConf{
        Writer: os.Stdout,
        Formatter: func(param cotton.LoggerFormatterParam, ctx *cotton.Context) string {
            return fmt.Sprintf("[INFO-STATIC] %v\t %d %s\n",
                param.TimeStamp.Format("2006/01/02 15:04:05"),
                param.StatusCode,
                filepath.Join(dir, ctx.Param("file")),
            )
        },
    })).Get("/*file", func(ctx *cotton.Context) {
        // file := filepath.Join(dir, ctx.Param("file"))

        // http.ServeFile(ctx.Response, ctx.Request, file)
        // // ctx.Response.GetStatusCode() for log
        // fmt.Println(ctx.Response.GetStatusCode(), file)

        http.StripPrefix("/static/", http.FileServer(http.Dir(dir))).ServeHTTP(ctx.Response, ctx.Request)
        // http.StripPrefix("", http.FileServer(nil)).ServeHTTP()
    })
    gs := r.Group("/s/")
    gs.StaticFile("/", dir, false)
    r.StaticFile("/m/", dir, true)
    r.Get("/panic", func(ctx *cotton.Context) {
        // i := 0
        // fmt.Println(1 / i)
        panic([]int{1, 2})
    })
    r.Get("/hello/", func(ctx *cotton.Context) {
        ctx.String(http.StatusOK, "hello get2")
    })
    // r.Use(cotton.LoggerWidthConf(cotton.LoggerConf{
    //  Formatter: func(param cotton.LoggerFormatterParam) string {
    //      return fmt.Sprintf("[info] %s %s %s\t%d %s\n",
    //          utils.TimeFormat(param.TimeStamp),
    //          param.ClientIP, param.Method, param.StatusCode,
    //          param.Path,
    //      )

    //  },
    // }))
    r.Get("/user/", func(ctx *cotton.Context) {
        ctx.String(http.StatusOK, "/user")
    })
    r.Get("/user/:name", func(ctx *cotton.Context) {
        ctx.String(http.StatusOK, "user name = "+ctx.Param("name"))
    })
    r.Get("/user/:name/:id", func(ctx *cotton.Context) {
        ctx.String(http.StatusOK, "user id = "+ctx.Param("id")+" name = "+ctx.Param("name"))
    })
    r.Get("/user/:name/:id/one", func(ctx *cotton.Context) {
        ctx.String(http.StatusOK, "one user id = "+ctx.Param("id")+" name = "+ctx.Param("name"))
    })
    r.Get("/user/:name/:id/two", func(ctx *cotton.Context) {
        ctx.String(http.StatusOK, "two user id = "+ctx.Param("id")+" name = "+ctx.Param("name"))
    })
    r.Get("/info/*file", func(ctx *cotton.Context) {
        ctx.String(http.StatusOK, "info file = "+ctx.Param("file"))
    })
    r.Post("/user/:id", func(ctx *cotton.Context) {
        ctx.String(http.StatusOK, "hello post "+ctx.Param("id"))
    })

    g1 := r.Group("/v1/", func(ctx *cotton.Context) {
        fmt.Println("g1 middleware")
    })
    g1.NotFound(func(ctx *cotton.Context) {
        ctx.String(http.StatusNotFound, "page ["+ctx.Request.RequestURI+"] not found")
    })
    {
        g1.Get("/a", func(ctx *cotton.Context) {
            ctx.String(http.StatusOK, "g1 a")
        })
        g1.Get("/info", func(ctx *cotton.Context) {
            ctx.JSON(http.StatusOK, cotton.M{
                "message": "from g1 info",
            })
        })
    }
    g2 := r.Group("/v2/")
    {
        g2.Get("/a", func(ctx *cotton.Context) {
            ctx.String(http.StatusOK, "g2 a "+ctx.Param("method"))
        })
        g2.Get("/b", func(ctx *cotton.Context) {
            ctx.String(http.StatusOK, "g2 b "+ctx.Param("method"))
        })
        g2.Get("/c/:id", func(ctx *cotton.Context) {
            ctx.String(http.StatusOK, "g2 c "+ctx.Param("method")+" id = "+ctx.Param("id"))
        })
    }

    g3 := r.Group("/v3/:method/")
    g3.Use(func(ctx *cotton.Context) {
        if ctx.Param("method") != "test" {
            ctx.Abort()
            ctx.String(http.StatusBadRequest, "no method test")
        }
    })
    {
        g3.Get("/a", func(ctx *cotton.Context) {
            ctx.String(http.StatusOK, "g3 a "+ctx.Param("method"))
        })
        g3.Get("/b", func(ctx *cotton.Context) {
            ctx.String(http.StatusOK, "g3 b "+ctx.Param("method"))
        })
        g3.Get("/c/:id", func(ctx *cotton.Context) {
            ctx.String(http.StatusOK, "g3 c "+ctx.Param("method")+" id = "+ctx.Param("id"))
        })
    }

    r.Group("/nohandle")
    r.Get("/redirect", func(ctx *cotton.Context) {
        urlto := ctx.GetDefaultQuery("url", "https://www.baidu.com")
        ctx.Redirect(302, urlto)
    })

    // r.PrintTree(http.MethodGet)
    r.Run(":5000")
}

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,761评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,953评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,998评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,248评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,130评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,145评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,550评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,236评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,510评论 1 291
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,601评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,376评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,247评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,613评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,911评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,191评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,532评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,739评论 2 335

推荐阅读更多精彩内容