V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
爱意满满的作品展示区。
honmaple
V2EX  ›  分享创造

基于 Go net/http 开发的轻量路由框架-forest

  •  
  •   honmaple · 2022-11-15 00:00:11 +08:00 · 2136 次点击
    这是一个创建于 739 天前的主题,其中的信息可能已经有所发展或是发生改变。

    很早就写完了,貌似还没分享过。

    之前阅读过 Gin ,Echo 的源码,觉得 Go 里面基于net/http写一个自己的路由框架还挺简单的,然后就动手了,没想到看起来容易,部分细节的处理还是挺麻烦的

    与 Gin 和 Echo 的区别

    Gin 和 Echo 我比较喜欢 Echo 的设计,所以很多地方都借鉴了 Echo ,比如 Context 是接口类型,每一个路由处理函数都会返回 error ,而 Gin 里借鉴里中间件的实现,使用一个列表变量+索引的方式实现,未使用闭包

    路由

    使用了自己写的 radix tree ,动态路由性能上可能比 Gin 和 Echo 弱了一丢丢,静态路由性能要高一丢丢,测试可见 https://github.com/honmaple/forest/tree/master/examples/benchmark

    • 兼容 Gin 和 Echo 的写法: /api/users/:id或者/api/files/*filename
    • 一个 URI 使用多个任意参数: /api/users/{id:int}/friends/{avatar:path},甚至path在前也可以,不过路由查找性能会下降
    • 正则路由: /api/users/{id:[0-9]+}
    • 可扩展的参数路由:
    import (
        "github.com/google/uuid"
    )
    
    type UUIDMatcher struct {
    }
    
    func (s *UUIDMatcher) Name() string {
        return "uuid"
    }
    
    func (s *UUIDMatcher) Match(path string, index int, next bool) (int, bool) {
        if index > 0 {
            return 0, false
        }
        if len(path) < 18 || (!next && len(path) > 18) {
            return 0, false
        }
        _, err := uuid.Parse(path[:18])
        if err != nil {
            return 0, false
        }
        return 18, true
    }
    
    func NewUUIDMatcher(rule string) forest.Matcher {
        return &UUIDMatcher{}
    }
    
    forest.RegisterRule("uuid", NewUUIDMatcher)
    
    router := forest.New()
    router.GET("/api/v1/user/{pk:uuid}", handler)
    

    参数绑定

    type Params struct {
        Text string `query:"text" json:"text" form:"text" param:"text"`
    }
    p := Params{}
    // bind query, method: not POST, PUT, PATCH
    // bind form or json or xml, method: POST, PUT, PATCH
    c.Bind(&p)
    // bind params, GET /test/:text
    c.BindParams(&p)
    // bind other params
    c.BindWith(&p, bind.Query)
    c.BindWith(&p, bind.Form)
    c.BindWith(&p, bind.MultipartForm)
    c.BindWith(&p, bind.JSON)
    c.BindWith(&p, bind.XML)
    c.BindWith(&p, bind.Params)
    c.BindWith(&p, bind.Header)
    // custom bind tag
    c.BindWith(&p, bind.FormBinder{"json"})
    c.BindWith(&p, bind.QueryBinder{"json"})
    

    中间件

    中间件借鉴了 Gin ,使用一个列表变量+索引,而不是 Echo 多个中间件嵌套闭包的方式

    func MyMiddleware(c forest.Context) error {
        // do something
        // c.Next() is required, or else your handler will not execute
        return c.Next()
    }
    router := forest.New()
    // with root
    router.Use(MyMiddleware)
    // with group
    group := router.Group(forest.WithPrefix("/api/v1"), forest.WithMiddlewares(MyMiddleware))
    // with special handler
    group.GET("/", MyMiddleware, func(c forest.Context) error {
        return nil
    })
    

    命名路由

    我自己的需求就是开发后台管理系统配置路由权限时为什么要手动输入每一条路由以及它们的 unique name 和描述,所以内置了几个变量,可以在定义时就对路由进行命名

    r := forest.New()
    g1 := r.Group(forest.WithPrefix("/api"), forest.WithName("g1"))
    g2 := g1.Group(forest.WithPrefix("/v1"), forest.WithName("g2"))
    r1 := g2.GET("/posts").Named("list_posts", "some description")
    r2 := g2.DELETE("/posts/:pk").Named("delete_post", "delete post with pk param")
    // result
    r.Route("g1.g2.list_posts") == r1
    r.URL("g1.g2.list_posts") == r1.URL() == "/v1/api/posts"
    r.Route("g1.g2.delete_post") == r2
    r.URL("g1.g2.delete_post", "12") == r2.URL("12") == "/v1/api/posts/12"
    

    想要获取全部路由,则可以遍历c.Forest().Routes()

    routes := c.Forest().Routes()
    ins := make([]forest.H, 0, len(routes))
    for _, r := range routes {
    	ins = append(ins, forest.H{
    		"id":     fmt.Sprintf("%s %s", r.Method(), r.Path()),
    		"name":   r.Name,
    		"path":   r.Path(),
    		"desc":   r.Desc(),
    		"method": r.Method(),
    	})
    }
    

    除此之外,还有静态文件,自定义错误,自定义 Context ,自定义子域名匹配等功能,有很多功能都是我自己在开发时的需求,觉得有用就增加到 forest 里面,有兴趣的可以看一下

    地址: https://github.com/honmaple/forest

    7 条回复    2022-11-18 16:17:48 +08:00
    sunorg
        1
    sunorg  
       2022-11-15 00:03:06 +08:00 via Android
    来个特别大的特点?
    honmaple
        2
    honmaple  
    OP
       2022-11-15 00:23:57 +08:00
    @sunorg 正则路由和可扩展参数路由不知道算不算,还有就是路径参数可以放在 url 中间,我看很多路由框架都是只能放到末尾;以及路由分组可挂载,不受主路由影响,方便扩展;其它的一些扩展库 session ,swagger 也有,不过这应该属于常规功能 https://github.com/honmaple/forest-contrib

    web 路由框架基本的功能都差不多,重量级的特点还未曾想到,如果有什么建议我可以看看能否实现
    murongxdb
        3
    murongxdb  
       2022-11-15 08:47:56 +08:00
    先来一个 star
    spatxos
        4
    spatxos  
       2022-11-15 08:49:42 +08:00
    已 star
    40EaE5uJO3Xt1VVa
        5
    40EaE5uJO3Xt1VVa  
       2022-11-15 09:24:26 +08:00
    第三个 star
    sunorg
        6
    sunorg  
       2022-11-15 13:47:04 +08:00 via Android
    @honmaple 不算,但必须来个 star 支持下。
    EZVIK
        7
    EZVIK  
       2022-11-18 16:17:48 +08:00
    最近也在学习写路由解析,star 学习一下
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2632 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 22ms · UTC 11:15 · PVG 19:15 · LAX 03:15 · JFK 06:15
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.