本文出自 https://geektutu.com/post/gee.html 对部分代码进行了修改并加上一些理解和调试图片
NOTE 大部分时候,我们需要实现一个 Web 应用,第一反应是应该使用哪个框架。不同的框架设计理念和提供的功能有很大的差别。比如 Python 语言的
django
和flask
,前者大而全,后者小而美。Go 语言/golang 也是如此,新框架层出不穷,比如Beego
,Gin
,Iris
等。那为什么不直接使用标准库,而必须使用框架呢?在设计一个框架之前,我们需要回答框架核心为我们解决了什么问题。只有理解了这一点,才能想明白我们需要在框架中实现什么功能。
net/http
提供了基础的 Web 功能,即监听端口,映射静态路由,解析 HTTP 报文。一些 Web 开发中简单的需求并不支持,需要手工实现。
- 动态路由:例如
hello/:name
,hello/*
这类的规则。- 鉴权:没有分组/统一鉴权的能力,需要在每个路由映射的 handler 中实现。
- 模板:没有统一简化的 HTML 机制。
- …
当我们离开框架,使用基础库时,需要频繁手工处理的地方,就是框架的价值所在。
Gee 本质上是对标准库的封装,先来看看标准库的用法
Gopackage main import ( "fmt" "log" "net/http" ) func main() { http.HandleFunc("/", indexHandler) http.HandleFunc("/hello", helloHandler) log.Fatal(http.ListenAndServe(":9999", nil)) } // handler echoes r.URL.Path func indexHandler(w http.ResponseWriter, req *http.Request) { fmt.Fprintf(w, "URL.Path = %q\n", req.URL.Path) } // handler echoes r.URL.Header func helloHandler(w http.ResponseWriter, req *http.Request) { for k, v := range req.Header { fmt.Fprintf(w, "Header[%q] = %q\n", k, v) } }
重点是这里
log.Fatal(http.ListenAndServe(":9999", nil))
点去实现
Gofunc ListenAndServe(addr string, handler Handler) error {
再点 Handler 跳转实现
Gotype Handler interface { ServeHTTP(ResponseWriter, *Request) }
通过查看net/http
的源码可以发现,Handler
是一个接口,需要实现方法 ServeHTTP ,也就是说,只要传入任何实现了 ServerHTTP 接口的实例,所有的 HTTP 请求,就都交给了该实例处理了。
因此搞一个有 ServeHTTP 方法的 Engine 类型就好了
Go// Engine is the uni handler for all requests type Engine struct{} func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { switch req.URL.Path { case "/": fmt.Fprintf(w, "URL.Path = %q\n", req.URL.Path) case "/hello": for k, v := range req.Header { fmt.Fprintf(w, "Header[%q] = %q\n", k, v) } default: fmt.Fprintf(w, "404 NOT FOUND: %s\n", req.URL) } } func main() { engine := new(Engine) log.Fatal(http.ListenAndServe(":9999", engine)) }
NOTE 在实现
Engine
之前,我们调用 http.HandleFunc 实现了路由和 Handler 的映射,也就是只能针对具体的路由写处理逻辑。比如/hello
。但是在实现Engine
之后,我们拦截了所有的 HTTP 请求,拥有了统一的控制入口。在这里我们可以自由定义路由映射的规则,也可以统一添加一些处理逻辑,例如日志、异常处理等。
使用层
Gopackage main import ( "fmt" "net/http" "gee" ) func main() { r := gee.New() r.GET("/", func(w http.ResponseWriter, req *http.Request) { fmt.Fprintf(w, "URL.Path = %q\n", req.URL.Path) }) r.GET("/hello", func(w http.ResponseWriter, req *http.Request) { for k, v := range req.Header { fmt.Fprintf(w, "Header[%q] = %q\n", k, v) } }) r.Run(":9999") }
Gopackage gee import ( "fmt" "net/http" ) // HandlerFunc defines the request handler used by gee type HandlerFunc func(http.ResponseWriter, *http.Request) // Engine implement the interface of ServeHTTP type Engine struct { router map[string]HandlerFunc } // New is the constructor of gee.Engine func New() *Engine { return &Engine{router: make(map[string]HandlerFunc)} } func (engine *Engine) addRoute(method string, pattern string, handler HandlerFunc) { key := method + "-" + pattern engine.router[key] = handler } // GET defines the method to add GET request func (engine *Engine) GET(pattern string, handler HandlerFunc) { engine.addRoute("GET", pattern, handler) } // POST defines the method to add POST request func (engine *Engine) POST(pattern string, handler HandlerFunc) { engine.addRoute("POST", pattern, handler) } // Run defines the method to start a http server func (engine *Engine) Run(addr string) (err error) { return http.ListenAndServe(addr, engine) } func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { key := req.Method + "-" + req.URL.Path if handler, ok := engine.router[key]; ok { handler(w, req) } else { fmt.Fprintf(w, "404 NOT FOUND: %s\n", req.URL) } }
NOTE
- 首先定义了类型
HandlerFunc
,这是提供给框架用户的,用来定义路由映射的处理方法。我们在Engine
中,添加了一张路由映射表router
,key 由请求方法和静态路由地址构成,例如GET-/
、GET-/hello
、POST-/hello
,这样针对相同的路由,如果请求方法不同,可以映射不同的处理方法(Handler),value 是用户映射的处理方法。- 当用户调用
(*Engine).GET()
方法时,会将路由和处理方法注册到映射表 router 中,(*Engine).Run()
方法,是 ListenAndServe 的包装。Engine
实现的 ServeHTTP 方法的作用就是,解析请求的路径,查找路由映射表,如果查到,就执行注册的处理方法。如果查不到,就返回 404 NOT FOUND 。
先看使用层
Gofunc main() { r := gee.New() r.GET("/", func(c *gee.Context) { c.HTML(http.StatusOK, "<h1>Hello Gee</h1>") }) r.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) }) r.POST("/login", func(c *gee.Context) { c.JSON(http.StatusOK, gee.H{ "username": c.PostForm("username"), "password": c.PostForm("password"), }) }) r.Run(":9999") }
为啥要设计 Context
NOTE 框架需要支持中间件,那中间件产生的信息放在哪呢?Context 随着每一个请求的出现而产生,请求的结束而销毁,和当前请求强相关的信息都应由 Context 承载。因此,设计 Context 结构,扩展性和复杂性留在了内部,而对外简化了接口。路由的处理函数,以及将要实现的中间件,参数都统一使用 Context 实例, Context 就像一次会话的百宝箱,可以找到任何东西。
给 handle 前先生成 context
Gofunc (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { c := newContext(w, req) engine.router.handle(c) }
在 Context 对一些常用的能力进行封装
Gofunc (c *Context) SetHeader(key string, value string) { c.Writer.Header().Set(key, value) } func (c *Context) String(code int, format string, values ...interface{}) { c.SetHeader("Content-Type", "text/plain") c.Status(code) c.Writer.Write([]byte(fmt.Sprintf(format, values...))) } func (c *Context) JSON(code int, obj interface{}) { c.SetHeader("Content-Type", "application/json") c.Status(code) encoder := json.NewEncoder(c.Writer) if err := encoder.Encode(obj); err != nil { http.Error(c.Writer, err.Error(), 500) } }
实现动态路由最常用的数据结构,被称为前缀树(Trie 树)。
tire.go
Gotype node struct { pattern string // 是否一个完整的url,不是则为空字符串 part string // URL块值,用/分割的部分,比如/abc/123,abc和123就是2个part children []*node // 该节点下的子节点 isWild bool // 是否模糊匹配,比如:filename或*filename这样的node就为true }
路由的相关流程有 2
Gofunc TestNew1(t *testing.T) { r := newRouter() r.addRoute("GET", "/", nil) r.addRoute("GET", "/hello/:name", nil) n, params := r.getRoute("GET", "/hello/geektutu") fmt.Printf("1: %v %v\n", n, params) fmt.Printf("2: %v\n", r.getRoutes("GET")) }
输出如下
CODE1: node{pattern=/hello/:name, part=:name, isWild=true} map[name:geektutu] 2: [node{pattern=/, part=, isWild=false} node{pattern=/hello/:name, part=:name, isWild=true}]
GET-/
的构建
JSON{ "pattern": "/", "part": "", "children": [], "isWild": false }
GET-/hello/:name
的构建
JSON{ "pattern": "/", "part": "", "children": [{ "pattern": "", "part": "hello", "children": [], "isWild": false }], "isWild": false }
JSON{ "pattern": "/", "part": "", "children": [ { "pattern": "", "part": "hello", "children": [ { "pattern": "/hello/:name", "part": ":name", "isWild": true } ], "isWild": false } ], "isWild": false }
在下一层递归调用 insert 的时候加上的 pattern
Gofunc (n *node) insert(pattern string, parts []string, height int) { if len(parts) == height { // 如果已经匹配完了,那么将pattern赋值给该node,表示它是一个完整的url // 这是递归的终止条件 n.pattern = pattern return }
Gon, params := r.getRoute("GET", "/hello/geektutu")
利用 node 中存储的 part 和 匹配传入的 parts 进行一层又一层的寻找
根据 pattern 和 searchParts 去做匹配的
Gofunc (r *router) getRoute(method string, path string) (*node, map[string]string) { searchParts := parsePattern(path) params := make(map[string]string) root, ok := r.roots[method] if !ok { return nil, nil } n := root.search(searchParts, 0) if n != nil { parts := parsePattern(n.pattern) for index, part := range parts { if part[0] == ':' { params[part[1:]] = searchParts[index] } if part[0] == '*' && len(part) > 1 { params[part[1:]] = strings.Join(searchParts[index:], "/") break } } return n, params } return nil, nil }
NOTE 分组控制(Group Control)是 Web 框架应提供的基础功能之一。所谓分组,是指路由的分组。如果没有路由分组,我们需要针对每一个路由进行控制。但是真实的业务场景中,往往某一组路由需要相似的处理。例如:
- 以
/post
开头的路由匿名可访问。- 以
/admin
开头的路由需要鉴权。- 以
/api
开头的路由是 RESTful 接口,可以对接第三方平台,需要三方平台鉴权。大部分情况下的路由分组,是以相同的前缀来区分的。因此,我们今天实现的分组控制也是以前缀来区分,并且支持分组的嵌套。例如
/post
是一个分组,/post/a
和/post/b
可以是该分组下的子分组。作用在/post
分组上的中间件(middleware),也都会作用在子分组,子分组还可以应用自己特有的中间件。中间件可以给框架提供无限的扩展能力,应用在分组上,可以使得分组控制的收益更为明显,而不是共享相同的路由前缀这么简单。例如
/admin
的分组,可以应用鉴权中间件;/
分组应用日志中间件,/
是默认的最顶层的分组,也就意味着给所有的路由,即整个框架增加了记录日志的能力。
使用层为
Gofunc main() { r := gee.New() r.GET("/index", func(c *gee.Context) { c.HTML(http.StatusOK, "<h1>Index Page</h1>") }) 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) }) } v2 := r.Group("/v2") { v2.GET("/hello/:name", func(c *gee.Context) { // expect /hello/geektutu c.String(http.StatusOK, "hello %s, you're at %s\n", c.Param("name"), c.Path) }) v2.POST("/login", func(c *gee.Context) { c.JSON(http.StatusOK, gee.H{ "username": c.PostForm("username"), "password": c.PostForm("password"), }) }) } r.Run(":9999") }
结构
Gotype ( 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 } )
GoEngine struct { *RouterGroup
这种写法类似 Go 中的继承,可参考理解
Go// New is the constructor of gee.Engine func New() *Engine { engine := &Engine{router: newRouter()} engine.RouterGroup = &RouterGroup{engine: engine} engine.groups = []*RouterGroup{engine.RouterGroup} return engine } // 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: group.prefix + prefix, parent: group, engine: engine, } engine.groups = append(engine.groups, newGroup) return newGroup } 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) } // GET defines the method to add GET request func (group *RouterGroup) GET(pattern string, handler HandlerFunc) { group.addRoute("GET", pattern, handler) } // POST defines the method to add POST request func (group *RouterGroup) POST(pattern string, handler HandlerFunc) { group.addRoute("POST", pattern, handler) }
GoRouterGroup struct { prefix string middlewares []HandlerFunc // support middleware
刚刚定义的 Group 能力为中间件的存放留了存储位置
重点是 在 router handle 的时候来使用中间件的逻辑
Gofunc (r *router) handle(c *Context) { n, params := r.getRoute(c.Method, c.Path) if n != nil { key := c.Method + "-" + n.pattern c.Params = params c.handlers = append(c.handlers, r.handlers[key]) } else { c.handlers = append(c.handlers, func(c *Context) { c.String(http.StatusNotFound, "404 NOT FOUND: %s\n", c.Path) }) } c.Next() }
Context 中对 中间件进行支持
Gotype Context struct { // origin objects Writer http.ResponseWriter Req *http.Request // request info Path string Method string Params map[string]string // response info StatusCode int handlers []HandlerFunc // 这里存放的不仅有 middleware,还有最终的处理函数 index int // 当前访问到哪个 hander 了 } func newContext(w http.ResponseWriter, req *http.Request) *Context { return &Context{ Path: req.URL.Path, Method: req.Method, Req: req, Writer: w, index: -1, } } func (c *Context) Next() { c.index++ s := len(c.handlers) for ; c.index < s; c.index++ { c.handlers[c.index](c) } }
Context 中的中间件是在 ServeHTTP
中对已有的 groups 进行扫描加入的
Gofunc (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { var middlewares []HandlerFunc for _, group := range engine.groups { if strings.HasPrefix(req.URL.Path, group.prefix) { middlewares = append(middlewares, group.middlewares...) } } c := newContext(w, req) c.handlers = middlewares engine.router.handle(c) }
以 / 路由的访问为例子,调用栈如下
利用 http.FileServer 来处理 assets 各种资源的获取
Gor.Static("/assets", "./static") func (group *RouterGroup) Static(relativePath string, root string) { handler := group.createStaticHandler(relativePath, http.Dir(root)) urlPattern := path.Join(relativePath, "/*filepath") // Register GET handlers group.GET(urlPattern, handler) }
NOTE
/assets/*filepath
,可以匹配/assets/
开头的所有的地址。例如/assets/js/geektutu.js
,匹配后,参数filepath
就赋值为js/geektutu.js
。
模板渲染这里也是 html/template 套一层官方的能力
结合代码理解并不复杂,略
Go// hello.go func test_recover() { defer func() { fmt.Println("defer func") if err := recover(); err != nil { fmt.Println("recover success") } }() arr := []int{1, 2, 3} fmt.Println(arr[4]) fmt.Println("after panic") } func main() { test_recover() fmt.Println("after recover") }
Bash$ go run hello.go defer func recover success after recover
利用 中间件的机制来完成错误恢复
Go// Default use Logger() & Recovery middlewares func Default() *Engine { engine := New() engine.Use(Logger(), Recovery()) return engine }