geektutu / blog

极客兔兔的博客,Coding Coding 创建有趣的开源项目。
https://geektutu.com
Apache License 2.0
167 stars 21 forks source link

Go语言动手写Web框架 - Gee第四天 分组控制Group | 极客兔兔 #44

Open geektutu opened 5 years ago

geektutu commented 5 years ago

https://geektutu.com/post/gee-day4.html

7天用 Go语言 从零实现Web框架教程(7 days implement golang web framework from scratch tutorial),用 Go语言/golang 动手写Web框架,从零实现一个Web框架,以 Gin 为原型从零设计一个Web框架。本文介绍了分组控制(Group Control)的意义,以及嵌套分组路由的实现。

wzc314 commented 4 years ago

很好的入门文章!感谢博主!

Day4中的Engine定义有一点疑问请教:

Engine struct {
    *RouterGroup
    router *router
    groups []*RouterGroup // store all groups
}

这个RouterGroup是Engine的一个属性吗?

Go接触不久,还请指教。

evanxg852000 commented 4 years ago

Wonderful article series, Thanks for sharing the knowledge. I cannot read the text but I have been using google translate to follow along & code. This type of article helps understand the concept without all the noise of a big project or optimisation. Please can you write an article and implementing a B+ tree for key value store in Go? a bit like the one in BoltDB.

evanxg852000 commented 4 years ago

@wzc314 很好的入门文章!感谢博主!

Day4中的Engine定义有一点疑问请教:

Engine struct {
  *RouterGroup
  router *router
  groups []*RouterGroup // store all groups
}

这个RouterGroup是Engine的一个属性吗?

Go接触不久,还请指教。

This is called embeded type in Go Read the section Embeding of this guide https://github.com/hoanhan101/ultimate-go

geektutu commented 4 years ago

@evanxg852000 I'm very glad to see your comment. I'm sorry and I'm busy writing the 3rd series, a ORM framework geeorm now. There is a golang version for bolt, named bbolt. I would like to imitate bbolt to implement geebolt, maybe in several weeks. It's a wonderful choice as the 4th, thanks for your suggestion.

geektutu commented 4 years ago

@wzc314 这是 go 中的嵌套类型,类似 Java/Python 等语言的继承。这样 Engine 就可以拥有 RouterGroup 的属性了。

201732110125 commented 4 years ago

感谢博主! 我想问一下,RouteGroup结构体中的parent属性是否可以删去, 因为在使用的的过程中,好像并没有需要 获取一个group的parent的地方。

geektutu commented 4 years ago

@201732110125 parent 之前设计是用来拼接 prefix 的,每个 group 只记录自己的部分,最后通过 parent 层层回溯拼接。不过后来改用 group.prefix + prefix 的方式 group 初始化时已经拼接了完整的 prefix,所以不需要 parent 了,可以删除。

xenv commented 4 years ago

我认为这里的 RouterGroup 应该嵌套 Engine 而不是反过来,gee.New() 的时候初始化一个 Engine,然后返回一个持有自己的 RouterGroup 就 OK。当然差别也不是特别大,不过从面向对象的角度舒服一点。


func New() *RouterGroup {
    engine := &Engine{router: newRouter()}
    return &RouterGroup{
        Engine: engine,
    }
}

func (group *RouterGroup) Group(prefix string) *RouterGroup {
    newGroup := &RouterGroup{
        prefix: group.prefix + prefix,
        Engine: group.Engine,
    }
    group.Engine.groups = append(group.Engine.groups , newGroup)
    return newGroup
}
geektutu commented 4 years ago

@xenv Go语言的嵌套在其他语言中类似于继承,子类必然是比父类有更多的成员变量和方法。RouterGroup 仅仅是负责分组路由,Engine 除了分组路由外,还有很多其他的功能。RouterGroup 继承 Engine 的 Run()ServeHTTP 等方法是没有意义的。

SourceLink commented 4 years ago

@geektutu @xenv Go语言的嵌套在其他语言中类似于继承,子类必然是比父类有更多的成员变量和方法。RouterGroup 仅仅是负责分组路由,Engine 除了分组路由外,还有很多其他的功能。RouterGroup 继承 Engine 的 Run()ServeHTTP 等方法是没有意义的。

type RouterGroup struct {
    prefix      string // 支持叠加
    middlewares []HandlerFunc
    router      *router
}

type Engine struct {
    *RouterGroup
}

func New() *Engine {
    engine := &Engine{}
    engine.RouterGroup = &RouterGroup{router: newRouter()}
    return engine
}

func (group *RouterGroup) Group(prefix string) *RouterGroup {
    newGroup := &RouterGroup{
        prefix: group.prefix + prefix,
        router: group.router,
    }
    return newGroup
}

感觉组合关系这样子好像比较容易理解, RouterGroup继承了router的路由功能, Engine继承了RouterGroup的分组功能, 同时还有其他的Run, ServeHTTP等接口功能

dont-see-big-shark commented 4 years ago

groups暂时没有作用,是否是后面使用?

How-invin commented 4 years ago

看文章学一半,看评论再学一半/xk

banzhihang commented 4 years ago

Engine 和 RouterGroup 是否属于循环嵌套了呢

sunanzhi commented 4 years ago

如果只是用前缀当分组的话会不会适用场景不高?我试了下如果是这个场景

v1.Group("v1")
v1.Use(gee.Logger())
v1.GET("/run", func(c *gee.Context) {
        c.Json(http.StatusOK,  student{})
    })
r.GET("/v1/say", func(c *gee.Context) {
    c.Json(http.StatusOK, student{})
})

那么 v1/say也会执行中间件的代码 所以将group设置的时候分开一下,用key去区分会不会适用场景多点?可以明确同prefix的情况下不同后缀也可以执行不同的middleware

// 分组配置
type GroupConfig struct {
    Prefix string // 前缀
    Key    string // 分组key
}

刚初学,问题有点多,见谅。

Licccccc commented 4 years ago

type ( RouterGroup struct { prefix string middlewares []HandlerFunc // support middleware parent RouterGroup // support nesting engine Engine // all groups share a Engine instance }

Engine struct {
    *RouterGroup
    router *router
    groups []*RouterGroup // store all groups
}

) go小白,这个type()是什么用法。。。望指点

Licccccc commented 4 years ago
geektutu commented 4 years ago

@sunanzhi 这个就看如何去设计了,前缀区分一般是比较好的方式。比如

对一个博客系统来说,/auth/user 下的走授权中间件,/post/<id>.html,就不走授权,走统计中间件。/api/ 开头的,是对外提供的公共接口,诸如此类。是比较符合 URL 设计的习惯的。

特别是 Restful API,以资源为中心的 URL 设计,通过前缀做不同业务的区分更为明显,不同前缀代表不同类型的资源。

fishjar commented 3 years ago

请教一下:

1、能不能在路由的前缀树节点中直接记录分组以及中间件函数呢?node中增加一个是否分组的标记,以及中间件列表。省去EngineRouterGroup相互引用

2、另外,我觉得路由的handler是否也可以直接挂载到路由节点中呢?

大概这样:

// router.go
type router struct {
    root    *node
}
// trie.go
type node struct {
    pattern     string
    part        string
    children    []*node
    isWild      bool
    isGroup     bool
    middlewares []HandlerFunc
    handlers    map[string]HandlerFunc
}
TuringBecky commented 3 years ago

@geektutu @201732110125 parent 之前设计是用来拼接 prefix 的,每个 group 只记录自己的部分,最后通过 parent 层层回溯拼接。不过后来改用 group.prefix + prefix 的方式 group 初始化时已经拼接了完整的 prefix,所以不需要 parent 了,可以删除。

如果去掉parent,在一个group中再创建一个group怎么办?

xlban163 commented 3 years ago

@How-invin 看文章学一半,看评论再学一半/xk

没错没错

geektutu commented 3 years ago

@fishjar 你的想法是没问题的,当前的这种实现,前缀树解耦得比较好,功能比较单一,容易替换为其他的前缀树实现。Gin 这部分一开始也是利用了第三方实现,后来替换为自己的。

geektutu commented 3 years ago

@echomuof 再创建子 group 的话,用当前 group 的 prefix 拼接应该就 OK 。

geektutu commented 3 years ago

@xlban163 哈哈,也感谢你的评论。

feixintianxia commented 3 years ago

@geektutu @sunanzhi 这个就看如何去设计了,前缀区分一般是比较好的方式。比如

对一个博客系统来说,/auth/user 下的走授权中间件,/post/<id>.html,就不走授权,走统计中间件。/api/ 开头的,是对外提供的公共接口,诸如此类。是比较符合 URL 设计的习惯的。

特别是 Restful API,以资源为中心的 URL 设计,通过前缀做不同业务的区分更为明显,不同前缀代表不同类型的资源。 同学, 可以这样理解, /auth 或 /user 是和使用者操作相关,服务器需要判断该使用者是否授权。 /post/.html 是网页自身需要统计一些信息触发的,不需要授权。

YellowPure commented 3 years ago

这里RouterGroup和Engine是循环引用吗? 在写的时候犯了个错误

把GET函数中的group.addRoute("GET", pattern, handler) 写成了group.engine.addRoute("GET", pattern, handler)

导致生成的prefix是空字符串,请问这是为什么呢?理论上group指针的prefix应该是有值的,小白求教

LAShZ commented 3 years ago

@YellowPure 这里RouterGroup和Engine是循环引用吗? 在写的时候犯了个错误

把GET函数中的group.addRoute("GET", pattern, handler) 写成了group.engine.addRoute("GET", pattern, handler)

导致生成的prefix是空字符串,请问这是为什么呢?理论上group指针的prefix应该是有值的,小白求教

调用group.engine.addRoute时,使用的是RouterGroup里面的engine结构从RouterGroup继承的addRoute方法,也就是说addRoute的作用对象是engine里面的RouteGroup指针指向的group变量。 观察函数gee.Group可以发现,当创建一个新的RouterGroup变量时,该变量内的engine结构实际上等于上一个group内的engine,而上一个engine内的RouterGroup指针指向的的group变量的prefix并没有加上当前Group函数传入的prefix,所以当你调用group.engine.addRoute时,得到的prefix实际上是调用gee.Group函数得到新的group之前的prefix。在给出的demo里,自然v1.engine.RouterGroup.prefix == ""

YellowPure commented 3 years ago

👍解答好详细,受教了。

Lavch notifications@github.com 于2021年2月6日周六 下午10:55写道:

@YellowPure https://github.com/YellowPure 这里RouterGroup和Engine是循环引用吗? 在写的时候犯了个错误

把GET函数中的group.addRoute("GET", pattern, handler) 写成了group.engine.addRoute("GET", pattern, handler)

导致生成的prefix是空字符串,请问这是为什么呢?理论上group指针的prefix应该是有值的,小白求教

调用group.engine.addRoute 时,使用的是RouterGroup里面的engine结构从RouterGroup继承的addRoute方法,也就是说addRoute的作用对象是engine里面的RouteGroup指针指向的group变量。 观察函数gee.Group 可以发现,当创建一个新的RouterGroup变量时,该变量内的engine结构实际上等于上一个group内的engine,而上一个engine内的RouterGroup指针指向的的group变量的prefix并没有加上当前Group函数传入的prefix,所以当你调用 group.engine.addRoute时,得到的prefix实际上是调用gee.Group函数得到新的group之前的prefix。在给出的demo里,自然v1.engine.RouterGroup.prefix == ""

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/geektutu/blog/issues/44#issuecomment-774489157, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABN2XRU2C3UJJKBJ5LSFGC3S5VJ5NANCNFSM4ISW2NUQ .

huchiZhang commented 3 years ago

有个问题,请问engine的结构体中为什么要包含RouterGroup,是做什么用的?

walkmiao commented 3 years ago

@huchiZhang 有个问题,请问engine的结构体中为什么要包含RouterGroup,是做什么用的?

group中肯定要包含engine来利用之前engine中已实现的router来完成路由分组,至于为什么engine要包含routergroup?我觉得也可以不包含 但是你每次用的时候 得先创建engine然后再创建group再开始注册handlerfunc这样做很别扭 engine作为web框架的引擎来说统一由它管理则是最好的 包括后面的中间件管理也都由engine来管理 面对用户则是干净整洁的接口 不知道说的对不对

Kimgtb commented 3 years ago

博主,好像:Engine 的 groups 没有用上?

rictt commented 3 years ago

小白请教:在main函数中通过gee.New()返回的实例是engine类型的,接着r.GET(),这时候r是engine类型,是怎么走到func (group *RouterGroup) GET这个方法中的

// main.go
func main() {
    r := gee.New()
    r.GET('/index', func() {})
}

// gee.go
// r.GET('/index') 为什么会走到这里呢
// 这里的接收者类型明明是RouterGroup
func (group *RouterGroup) GET(path string, handler HandlerFunc) {
    fmt.Println("I'm in group GET, path is ", path)
    pattern := group.prefix + path
    group.addRoute("GET", pattern, handler)
}
LCAR979 commented 3 years ago
> @RiCTT > 小白请教:在main函数中通过gee.New()返回的实例是engine类型的,接着r.GET(),这时候r是engine类型,是怎么走到func (group *RouterGroup) GET这个方法中的 > > ```go > // main.go > func main() { > r := gee.New() > r.GET('/index', func() {}) > } > > // gee.go > // r.GET('/index') 为什么会走到这里呢 > // 这里的接收者类型明明是RouterGroup > func (group *RouterGroup) GET(path string, handler HandlerFunc) { > fmt.Println("I'm in group GET, path is ", path) > pattern := group.prefix + path > group.addRoute("GET", pattern, handler) > } > > ```

可以看一下这里 https://eli.thegreenplace.net/2020/embedding-in-go-part-1-structs-in-structs/。 Engine类型的定义里包括了*RouterGroup,所以以*RouterGroup作为接收者的方法,Engine类型一样可以调用,可以显式的改写为:

// from embedded struct to explicit form
type Engine struct {
    rg *RouterGroup
    router *Router
    groups []*RouterGroup
}
func (engine *Engine) GET(path string, handler HandlerFunc) {
    engine.rg.addRoute("GET", pattern, handler)
}

正如参考文章中的第一个例子中列举的,这种embedding的方式给engine提供了添加路由策略的能力,同时不需要像上面这样增加额外的转换调用函数。

chanLi-s commented 3 years ago

// Group is defined to create a new RouterGroup // remember all groups share the same Engine instance func (group RouterGroup) Group(prefix string) RouterGroup { engine := group.engine newGroup := &RouterGroup{ prefix: engine.groups[len(engine.groups)-1].prefix + prefix, parent: group, engine: engine, } engine.RouterGroup=newGroup //这里加上一句那么每次进来的路由分组如果重新实例New了就会重新置空这个group,那么v2 := New().Group("/v2"),外层这样写进来就等于每一组有一个前缀实例,达到博主的测试结果,这个争议点在于分组时需不需要重新实例 engine.groups = append(engine.groups, newGroup)

return newGroup

}

chanLi-s commented 3 years ago

@RiCTT 小白请教:在main函数中通过gee.New()返回的实例是engine类型的,接着r.GET(),这时候r是engine类型,是怎么走到func (group *RouterGroup) GET这个方法中的

// main.go
func main() {
    r := gee.New()
    r.GET('/index', func() {})
}

// gee.go
// r.GET('/index') 为什么会走到这里呢
// 这里的接收者类型明明是RouterGroup
func (group *RouterGroup) GET(path string, handler HandlerFunc) {
  fmt.Println("I'm in group GET, path is ", path)
  pattern := group.prefix + path
  group.addRoute("GET", pattern, handler)
}
chanLi-s commented 3 years ago

因为你New出来的实例中包含Group对象,在调用engine下的group也就会执行到这个get方法

galaxyzen commented 3 years ago

感觉可以把Engine和RouterGroup合并为一个类型,比如叫做"Dispatcher"之类的

type Dispatcher struct {
    basePath string
    router *router
    middlewares []HandlerFunc
}
jiadamao commented 2 years ago

@banzhihang Engine 和 RouterGroup 是否属于循环嵌套了呢

我也想知道这个问题

karmanluo commented 2 years ago

curl "http://localhost:9999/v2/hello/geektutu" hello geektutu, you're at /hello/geektutu

结果应该是: hello geektutu, you're at /v2/hello/geektutu

zmf173417897 commented 2 years ago

@jiadamao

@banzhihang Engine 和 RouterGroup 是否属于循环嵌套了呢

我也想知道这个问题

我也想知道

zmf173417897 commented 2 years ago

@jiadamao

@banzhihang Engine 和 RouterGroup 是否属于循环嵌套了呢

我也想知道这个问题

不知道算不算嵌套,Engine算是根Group,里面包含一个groups队列,包含多个分组,每个group里面相当于有一个engine的指针

DurantVivado commented 2 years ago

@zmf173417897

@jiadamao

@banzhihang Engine 和 RouterGroup 是否属于循环嵌套了呢

我也想知道这个问题

不知道算不算嵌套,Engine算是根Group,里面包含一个groups队列,包含多个分组,每个group里面相当于有一个engine的指针

我猜你应该想说子类继承父类,子类继承子类这种模式。但实际上Engine本身是指针,它本身继承了RouterGroup, RouterGroup右包含一个engine成员,你可以理解为双向链表。

DurantVivado commented 2 years ago
v1 := r.Group("/v1")
    {
        v1.GET("/", func(c *gee.Context) {
            c.HTML(http.StatusOK, "<h1>Hello Gee</h1>")
        })

        v1.GET("/hello", func(c *gee.Context) {
            // expect /hello?name=geektutu
            c.String(http.StatusOK, "hello %s, you're at %s\n", c.Query("name"), c.Path)
        })
    }

这里的大花括号是出于可读性设计的,与Group无关。

AnthonyYangU commented 2 years ago

@chanLi-s

@RiCTT 小白请教:在main函数中通过gee.New()返回的实例是engine类型的,接着r.GET(),这时候r是engine类型,是怎么走到func (group *RouterGroup) GET这个方法中的

// main.go
func main() {
    r := gee.New()
    r.GET('/index', func() {})
}

// gee.go
// r.GET('/index') 为什么会走到这里呢
// 这里的接收者类型明明是RouterGroup
func (group *RouterGroup) GET(path string, handler HandlerFunc) {
    fmt.Println("I'm in group GET, path is ", path)
    pattern := group.prefix + path
    group.addRoute("GET", pattern, handler)
}

因为Engine继承了RouterGroup

liujing-siyang commented 2 years ago

RouterGroup实现了Group方法,而Engine 拥有 RouterGroup,那RouterGroup不论保不保存Engine的指针,RouterGroup和Engine都可以添加路由吧

fatFire commented 2 years ago

@SourceLink

@geektutu @xenv Go语言的嵌套在其他语言中类似于继承,子类必然是比父类有更多的成员变量和方法。RouterGroup 仅仅是负责分组路由,Engine 除了分组路由外,还有很多其他的功能。RouterGroup 继承 Engine 的 Run()ServeHTTP 等方法是没有意义的。

type RouterGroup struct {
  prefix      string // 支持叠加
  middlewares []HandlerFunc
  router      *router
}

type Engine struct {
  *RouterGroup
}

func New() *Engine {
  engine := &Engine{}
  engine.RouterGroup = &RouterGroup{router: newRouter()}
  return engine
}

func (group *RouterGroup) Group(prefix string) *RouterGroup {
  newGroup := &RouterGroup{
      prefix: group.prefix + prefix,
      router: group.router,
  }
  return newGroup
}

感觉组合关系这样子好像比较容易理解, RouterGroup继承了router的路由功能, Engine继承了RouterGroup的分组功能, 同时还有其他的Run, ServeHTTP等接口功能

感觉你这样写更好理解一些

Allsochen commented 2 years ago

all groups share a Engine instance应该修改为all groups share an Engine instance

heql0668 commented 2 years ago

首先,非常感谢大神写的教程,受益匪浅,学习到了很多,非常感谢,已打赏。通过阅读此章节,我发现关于分组的这块是比较绕的,有点难理解,在此我提出一些我觉得更好的优化建议

基于以上的职责模块划分,这里贴出我觉得可以改进的模块

group.go

package gee

import (
    "log"
)

type RouterGroup struct {
    *router
    prefix      string
    parent      *RouterGroup
    middlewares []*HandlerFunc
}

// 每一个路由的路径肯定是有根的, 那么可以创建一个默认的根路由分组,新加分组必定是基于根路由分组进行新增的
func newRootGroup() *RouterGroup {
    return &RouterGroup{
        prefix: "",
        router: NewRouter(),
    }
}

// 所有分组已经路由相关的管理操作的代码,不应该放置到gee.go

func (group *RouterGroup) Group(prefix string) *RouterGroup {
    prefix = group.prefix + prefix
    newGroup := &RouterGroup{
        prefix: prefix,
        parent: group,
        router: group.router,
    }
    return newGroup
}

// 这里类似面向对象编程一样重写router.addRoute
func (group *RouterGroup) addRoute(method string, pattern string, handler HandlerFunc) {
    pattern = group.prefix + pattern
    log.Printf("Route %4s - %s\n", method, pattern)
  // 调用父类的addRoute
    group.router.addRoute(method, pattern, handler)
}

func (group *RouterGroup) GET(pattern string, handler HandlerFunc) {
    group.addRouter("GET", pattern, handler)
}

func (group *RouterGroup) POST(pattern string, handler HandlerFunc) {
    group.addRouter("POST", pattern, handler)
}

gee.go

package gee

import "net/http"

type Engine struct {
    *RouterGroup // 可以看做是继承底层模块所拥有的能力
    groups []*RouterGroup
}

func New() *Engine {
    group := newRootGroup()
    engine := &Engine{
        RouterGroup: group,
        groups:      []*RouterGroup{group},
    }
    return engine
}

// engine想知道自己创建了多少个分组, 这里就应该由它自己来进行统计, 这是engine的职责, 而不是RouterGroup的
func (e *Engine) Group(prefix string) *RouterGroup {
    group := e.RouterGroup.Group(prefix)
    e.groups = append(e.groups, group)
    return group
}

func (e *Engine) Run(addr string) (err error) {
    return http.ListenAndServe(addr, e)
}

func (e *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    c := newContext(w, req)
    e.handle(c) // 使用底层模块提供的能力
}
vfmh commented 2 years ago

RouterGroup里面为什么需要*Engine呀,感觉没啥用呀

sphierex commented 2 years ago

@bjpwg 博主,好像:Engine 的 groups 没有用上?

GroupRouter 通过 engine 实例,将自身的路由注册到 handlers 里。

func (group *RouterGroup) addRoute(method string, comp string, handler HandlerFunc) {
    pattern := group.prefix + comp
    log.Printf("Route %4s - %s", method, pattern)
    // 这里
    group.engine.router.addRoute(method, pattern, handler)
}
abcdhope commented 2 years ago
func (group *RouterGroup) Group(prefix string) *RouterGroup {
    engine := group.engine
    newGroup := &RouterGroup{
        prefix: group.prefix + prefix,
        parent: group,
        engine: engine,
    }
    engine.groups = append(engine.groups, newGroup)
    return newGroup
}

中的engine := group.engine是否可以去除而直接使用group.engine呢?