本文通过回答关键问题的方式,记录阅读 gee 代码过程中的思考,并做出补充和改进,其中有所借鉴 gin 框架。
参考 gee 的方式,共设置七个模块,每个模块相互独立并依次迭代,源码在 implement-from-scratch 。下面从如何设计框架的角度,提出了一些问题。
0.序言 net/http 可以干什么 提供基础的 web 功能:
监听端口 映射静态路由 请求处理(解析 HTTP 报文) 响应处理(生成和发送HTTP报文) 1 2 3 4 5 6 7 8 func main () { http.HandleFunc("/" , handler) http.ListenAndServe(":9999" , nil )) }func handler (w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "URL.Path = %q\n" , r.URL.Path) }
Web 框架需要干什么 换句话说,需要 web 框架在 net/http 的基础上实现什么?
路由(Routing):将请求映射到函数,支持动态路由(例如'/hello/:name
)、支持路由分组 上下文(Context):Context 随着每一个请求的出现而产生,请求的结束而销毁,和当前请求强相关的信息都应由 Context 承载。为请求封装上下文,可以简化接口调用(不然需要带上 http.Request、http.ResponseWriter;还可以保存中间件产生的中间信息)。 中间件(Midddleware):提供通用的中间件、为用户自定义中间件提供插入点。 模板(Templates):使用内置模板引擎提供模板渲染机制。 错误恢复(Panic Recover):提供错误处理机制,防止 panic 导致服务宕掉。 1.HTTP 基础 如何接管 HTTP 请求 在 http.ListenAndServe(":9999", nil))
中第二个参数,表示处理所有 HTTP 请求的实例。如果传入 nil,则使用默认的 http.DefaultServeMux。
1 2 3 4 5 6 type Server struct { Addr string Handler Handler
只需要不传入 nil,传入我们自己的实例,就可以接管所有 HTTP 请求,开始构建我们的 web 框架。
那该传入什么呢?先看看 net/http 是如何实现的。第二个参数的类型是 Handler 接口类型,接口定义如下。
1 2 3 4 type Handler interface { ServeHTTP(ResponseWriter, *Request) }
在 net/http/server.go 中,HandlerFunc 实现了这个 Handler 接口。实现如下。
注:这里插一下,HandlerFunc 是一个函数类型,在 go 中,函数是第一公民,也是可以实现接口的。第一次看这个写法可能比较奇怪,函数类型 HandlerFunc 调用一个函数 ServeHTTP,竟然最后还是调用自己 f(w, r)
。为什么要这么做?可以阅读一下这篇文章《Go 接口型函数的使用场景 》。这里总结一下,**为了既可以传入任何函数,也可以传入结构体,**以 HandlerFunc 为例:
参数类型是 Handler 接口类型,普通函数/匿名函数 只需要强制转换为 HandlerFunc,就可以作为参数传入) 只要实现了 ServeHTTP 方法的结构体 ,也可以作为参数传入。 1 2 3 4 5 6 7 8 9 10 11 type HandlerFunc func (ResponseWriter, *Request) func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { f(w, r) }
回到主线,也就是说,只要我们像 HandlerFunc 一样,实现了 Handler 接口,就可以作为 http.ListenAndServe 第二个参数,从而接管监听到的所有 HTTP 请求。
定义 Engine,并实现结构体函数 ServeHTTP。将 http.ListenAndServe 的参数传递,封装到 Run 函数中,完成。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 type Engine struct {}func New () *Engine { return &Engine{} }func (engine *Engine) Run(addr string ) error { return http.ListenAndServe(addr, engine) }func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { fmt.Println(req) }func main () { engine := gee.New() _ = engine.Run(":9999" ) }
为什么 ServeHTTP 参数分别是指针传递/值传递 1 2 3 4 5 func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { }
req 接收的是结构体,可能有大量数据,使用指针可以节省内存。
http.ResponseWriter 是一个接口类型,w 是接口类型变量,不能使用指针。
目的是,不管接收的是什么,只要它实现了 Header()
、Write([]byte)
、WriteHeader(statusCode int)
这三个函数,它就可以保存在 w 这个变量中。(也没必要使用指针,这里需要得是 w 的三个方法。)
1 2 3 4 5 6 7 8 type ResponseWriter interface { Header() Header Write([]byte ) (int , error ) WriteHeader(statusCode int ) }
如何管理静态路由及请求处理函数 框架使用者想要什么?想要用这样的方式注册静态路由。
1 2 3 4 5 6 7 8 9 10 11 func main () { engine := gee.New() engine.GET("/" , Index) _ = engine.Run(":9999" ) }func Index (w http.ResponseWriter, req *http.Request) { }
我们在 Engine 内部维护一个 map,用来映射路由地址和处理函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 type HandlerFunc func (http.ResponseWriter, *http.Request) type Engine struct { router map [string ]HandlerFunc }func New () *Engine { return &Engine{ router: make (map [string ]HandlerFunc), } }
在 map 中,使用 method + "-" + pattern
作为 key,HandlerFunc 作为 value。提供 addRoute 函数增加 map 中的路由映射。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 func (engine *Engine) addRoute(method string , pattern string , handler HandlerFunc) { key := method + "-" + pattern engine.router[key] = handler }func (engine *Engine) GET(pattern string , handler HandlerFunc) { engine.addRoute("GET" , pattern, handler) }func (engine *Engine) POST(pattern string , handler HandlerFunc) { engine.addRoute("POST" , pattern, handler) }
最后需要改写 ServeHTTP 的逻辑。收到请求后,用请求方法和请求路径构造 key,并执行对应的请求处理函数。
1 2 3 4 5 6 7 8 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) } }
完整代码 1 2 3 4 5 6 7 version_1_http ├── gee │ └── gee.go ├── go.mod └── main.go
gee/gee.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 package geeimport ( "fmt" "net/http" )type HandlerFunc func (http.ResponseWriter, *http.Request) type Engine struct { router map [string ]HandlerFunc }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 }func (engine *Engine) GET(pattern string , handler HandlerFunc) { engine.addRoute("GET" , pattern, handler) }func (engine *Engine) POST(pattern string , handler HandlerFunc) { engine.addRoute("POST" , pattern, handler) }func (engine *Engine) Run(addr string ) 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) } }
main.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package mainimport ( "fmt" "net/http" "version_1_http/gee" )func main () { engine := gee.New() engine.GET("/" , func (w http.ResponseWriter, req *http.Request) { fmt.Fprintf(w, "URL.Path = %q\n" , req.URL.Path) }) engine.GET("/hello" , func (w http.ResponseWriter, req *http.Request) { for k, v := range req.Header { fmt.Fprintf(w, "Header[%q] = %q\n" , k, v) } }) err := engine.Run(":9999" ) if err != nil { panic (err) } }
2. 上下文 需要 context 做什么 context 随着每一个请求的出现而产生,请求的结束而销毁,和当前请求强相关的信息都应由 Context 承载。将扩展性和复杂性留在了 context 内部,而对外提供简化的接口。
简化接口参数:context 储存 http.Request、http.ResponseWriter,让请求处理函数的参数、中间件的参数,均使用 context 实例。 封装常用方法:http.Request、http.ResponseWriter 提供的接口粒度太细,用起来繁琐。 封装获取请求参数的方法。 封装快速构造 String/Data/JSON/HTML 响应的方法。 封装设置响应的 header(状态码 StatusCode 和消息类型 ContentType)的方法。 储存上下文信息 如何简化接口参数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 type Context struct { Req *http.Request Writer http.ResponseWriter Path string Method string StatusCode int }func NewContext (writer http.ResponseWriter, req *http.Request) *Context { return &Context{ Req: req, Writer: writer, Path: req.URL.Path, Method: req.Method, } }
将请求处理函数的参数,由原来的 http.ResponseWriter、*http.Request 改成 context。 如何封装常用方法 主要是封装 http 的原生功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 func (c *Context) PostForm(key string ) string { return c.Req.FormValue(key) }func (c *Context) Query(key string ) string { return c.Req.URL.Query().Get(key) }func (c *Context) SetHeader(key string , value string ) { c.Writer.Header().Set(key, value) }func (c *Context) Status(code int ) { c.StatusCode = code c.Writer.WriteHeader(code) }func (c *Context) String(code int , format string , values ...interface {}) { c.SetHeader("Content-Type" , "text/plain" ) c.Status(code) if _, err := c.Writer.Write([]byte (fmt.Sprintf(format, values...))); err != nil { http.Error(c.Writer, err.Error(), 500 ) } }func (c *Context) JSON(code int , obj interface {}) { c.SetHeader("Context-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 ) } }func (c *Context) Data(code int , date []byte ) { c.Status(code) if _, err := c.Writer.Write(date); err != nil { http.Error(c.Writer, err.Error(), 500 ) } }func (c *Context) HTML(code int , html string ) { c.SetHeader("Context-Type" , "text/html" ) c.Status(code) if _, err := c.Writer.Write([]byte (html)); err != nil { http.Error(c.Writer, err.Error(), 500 ) } }
完整代码 注:为便于动态路由的处理,我们将 router 的这个 map,单独提取出来。
1 2 3 4 5 6 7 8 9 version_2_context ├── gee │ ├── context.go │ ├── gee.go │ └── router.go ├── go.mod └── main.go
gee/gee.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 package geeimport ( "net/http" )type HandlerFunc func (ctx *Context) type H map [string ]interface {}type Engine struct { router *router }func New () *Engine { return &Engine{ router: newRouter(), } }func (engine *Engine) GET(pattern string , handler HandlerFunc) { engine.router.addRoute("GET" , pattern, handler) }func (engine *Engine) POST(pattern string , handler HandlerFunc) { engine.router.addRoute("POST" , pattern, handler) }func (engine *Engine) Run(addr string ) error { return http.ListenAndServe(addr, engine) }func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { context := NewContext(w, req) engine.router.handle(context) }
gee/router.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 package geeimport "fmt" type router struct { handlers map [string ]HandlerFunc }func newRouter () *router { return &router{ handlers: make (map [string ]HandlerFunc), } }func (r *router) addRoute(method string , pattern string , handler HandlerFunc) { key := method + "-" + pattern r.handlers[key] = handler }func (r *router) handle(c *Context) { key := c.Req.Method + "-" + c.Req.URL.Path if handler, ok := r.handlers[key]; ok { handler(c) } else { fmt.Fprintf(c.Writer, "404 NOT FOUND: %s\n" , c.Req.URL) } }
gee/context.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 package geeimport ( "encoding/json" "fmt" "net/http" )type Context struct { Req *http.Request Writer http.ResponseWriter Path string Method string StatusCode int }func NewContext (writer http.ResponseWriter, req *http.Request) *Context { return &Context{ Req: req, Writer: writer, Path: req.URL.Path, Method: req.Method, } }func (c *Context) PostForm(key string ) string { return c.Req.FormValue(key) }func (c *Context) Query(key string ) string { return c.Req.URL.Query().Get(key) }func (c *Context) SetHeader(key string , value string ) { c.Writer.Header().Set(key, value) }func (c *Context) Status(code int ) { c.StatusCode = code c.Writer.WriteHeader(code) }func (c *Context) String(code int , format string , values ...interface {}) { c.SetHeader("Content-Type" , "text/plain" ) c.Status(code) if _, err := c.Writer.Write([]byte (fmt.Sprintf(format, values...))); err != nil { http.Error(c.Writer, err.Error(), 500 ) } }func (c *Context) JSON(code int , obj interface {}) { c.SetHeader("Context-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 ) } }func (c *Context) Data(code int , date []byte ) { c.Status(code) if _, err := c.Writer.Write(date); err != nil { http.Error(c.Writer, err.Error(), 500 ) } }func (c *Context) HTML(code int , html string ) { c.SetHeader("Context-Type" , "text/html" ) c.Status(code) if _, err := c.Writer.Write([]byte (html)); err != nil { http.Error(c.Writer, err.Error(), 500 ) } }
mian.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 package mainimport ( "net/http" "version_2_context/gee" )func main () { engine := gee.New() engine.GET("/" , func (ctx *gee.Context) { ctx.HTML(http.StatusOK, "<h1>hello Gee</h1>" ) }) engine.GET("/hello" , func (ctx *gee.Context) { ctx.String(http.StatusOK, "hello %s" , ctx.Query("name" )) }) engine.POST("/login" , func (ctx *gee.Context) { ctx.JSON(http.StatusOK, gee.H{ "username" : ctx.PostForm("username" ), "password" : ctx.PostForm("password" ), }) }) err := engine.Run(":9999" ) if err != nil { panic (err) } }
3.前缀树路由 为什么需要前缀树路由 之前使用 map 存储路由表,但是请求路径都是确定的,无法支持动态路由的匹配,比如用 /hello/:name
匹配 /hello/aimtao
。
使用前缀树结构后,可以将一个请求地址 /hello/:name
,以 “/” 为分割符分为不同段 hello
、aimtao
,依次进行匹配判断。
如何实现前缀树 在实现前缀树路由之前,我们先回顾一下,如何实现一颗前缀树。比如在要插入 AB、ABC、DF、DH、XY 五个字符串。
实现前缀树需要做三件事:设计 Node 节点、实现前缀树的插入、实现前缀树的搜索。
每个节点应该存哪些内容呢?
需要一个 bool 型 isEnd 来标记,当前节点的字符是否是一个单词的结尾。 每个节点都使用 map 存子节点,便于快速查找下一个字符的 Node 节点。 其实并不需要储存当前节点代表哪个字符,因为父节点的 map 中已保存。 1 2 3 4 5 6 7 8 9 type Node struct { isEnd bool next map [rune ]*Node }type Trie struct { root *Node }
所以在前缀树中插入 AB、ABC、DF、DH、XY 五个字符串,实际上是一个这样的树。
确定好 Node 结构,再来看看如何插入字符串。
1 2 3 4 5 6 7 8 9 10 11 12 func (trie *Trie) Insert(word string ) { cur := trie.root for _, char := range []rune (word) { if _, ok := cur.next[char]; !ok { cur.next[char] = &Node{next: make (map [rune ]*Node)} } cur = cur.next[char] } cur.isEnd = true }
查询的时候,和插入流程是一样的,区别在于,插入时,map 中没有则创建,查询时,map 中没有则返回 false。
1 2 3 4 5 6 7 8 9 10 11 12 func (trie *Trie) Search(word string ) bool { cur := trie.root for _, char := range []rune (word) { if _, ok := cur.next[char]; !ok { return false } cur = cur.next[char] } return cur.isEnd }
测试代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 package draftimport ( "fmt" "testing" )func TestTrie (t *testing.T) { trie := Trie{ root: &Node{next: make (map [rune ]*Node)}, } trie.InsertMore("AB" , "ABC" , "DF" , "DH" , "XY" ) Print(trie.root) }func Print (node *Node) { fmt.Printf("Node{isEnd:%t, next:[" , node.isEnd) n := len (node.next) i := 0 for k, v := range node.next { fmt.Printf("'%c': %p" , k, v) i++ if i < n { fmt.Printf(", " ) } } fmt.Println("]}" ) for _, v := range node.next { Print(v) } }
如何实现前缀树路由 路由地址由 /
进行分隔,例如注册三个 GET 请求 “/hello”、“/hello/:name”、“/assets/*filepath”,一个POST 请求 “/hello”,前缀树应该是这样的。
和实现前缀树一样,实现前缀树路由需要做三件事:设计 Node 节点、实现前缀树路由的插入,实现前缀树路由的查找。
前缀树路由 Node 节点,需要保存四个字段,各有作用。
1 2 3 4 5 6 7 8 type node struct { path string part string children map [string ]*node isWild bool }
例如配置路由地址 “/hello”、“/hello/:name”、“/assets/*filepath” 时,实际上的前缀树如图。
图
将路由地址按 /
进行分隔。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 func parsePath (pattern string ) []string { patterns := strings.Split(pattern, "/" ) if len (patterns) > 0 && patterns[0 ] == "" { patterns = patterns[1 :] } if len (patterns) > 0 && patterns[len (patterns)-1 ] == "" { patterns = patterns[:len (patterns)-1 ] } return patterns }
插入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 func (root *node) insert(pattern string ) { cur := root patterns := parsePath(pattern) for _, part := range patterns { if _, ok := cur.children[part]; !ok { cur.children[part] = &node{ part: part, children: make (map [string ]*node), isWild: part[0 ] == ':' || part[0 ] == '*' , } } cur = cur.children[part] } cur.path = pattern }
查询
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 func (root *node) search(pattern string ) (*node, map [string ]string ) { params := make (map [string ]string ) cur := root patterns := parsePath(pattern) for _, part := range patterns { if cur.children[part] == nil { for k, v := range cur.children { if v.isWild == true && k[0 ] == '*' { params[k[1 :]] = pattern[strings.Index(pattern, part):] return v, params } else if v.isWild == true && k[0 ] == ':' { params[k[1 :]] = part cur = v break } else { return nil , nil } } } else { cur = cur.children[part] } } if cur.path != "" { return cur, params } return nil , nil }
完整代码 1 2 3 4 5 6 7 8 9 10 11 12 13 version_3_router ├── gee │ ├── context.go │ ├── gee.go │ ├── router.go │ ├── trie.go │ └── draft │ ├── tire.go │ └── trie_test.go ├── go.mod └── main.go
gee/gee.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 package geeimport ( "net/http" )type HandlerFunc func (ctx *Context) type H map [string ]interface {}type Engine struct { router *router }func New () *Engine { return &Engine{ router: newRouter(), } }func (engine *Engine) GET(pattern string , handler HandlerFunc) { engine.router.addRoute("GET" , pattern, handler) }func (engine *Engine) POST(pattern string , handler HandlerFunc) { engine.router.addRoute("POST" , pattern, handler) }func (engine *Engine) Run(addr string ) error { return http.ListenAndServe(addr, engine) }func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { context := NewContext(w, req) engine.router.handle(context) }
gee/context.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 package geeimport ( "encoding/json" "fmt" "net/http" )type Context struct { Req *http.Request Writer http.ResponseWriter Path string Method string Params map [string ]string StatusCode int }func NewContext (writer http.ResponseWriter, req *http.Request) *Context { return &Context{ Req: req, Writer: writer, Path: req.URL.Path, Method: req.Method, } }func (c *Context) PostForm(key string ) string { return c.Req.FormValue(key) }func (c *Context) Query(key string ) string { return c.Req.URL.Query().Get(key) }func (c *Context) Param(key string ) string { return c.Params[key] }func (c *Context) SetHeader(key string , value string ) { c.Writer.Header().Set(key, value) }func (c *Context) Status(code int ) { c.StatusCode = code c.Writer.WriteHeader(code) }func (c *Context) String(code int , format string , values ...interface {}) { c.SetHeader("Content-Type" , "text/plain" ) c.Status(code) if _, err := c.Writer.Write([]byte (fmt.Sprintf(format, values...))); err != nil { http.Error(c.Writer, err.Error(), 500 ) } }func (c *Context) JSON(code int , obj interface {}) { c.SetHeader("Context-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 ) } }func (c *Context) Data(code int , date []byte ) { c.Status(code) if _, err := c.Writer.Write(date); err != nil { http.Error(c.Writer, err.Error(), 500 ) } }func (c *Context) HTML(code int , html string ) { c.SetHeader("Context-Type" , "text/html" ) c.Status(code) if _, err := c.Writer.Write([]byte (html)); err != nil { http.Error(c.Writer, err.Error(), 500 ) } }
gee/router.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 package geeimport "fmt" type router struct { roots map [string ]*node handlers map [string ]HandlerFunc }func newRouter () *router { return &router{ roots: make (map [string ]*node), handlers: make (map [string ]HandlerFunc), } }func (r *router) addRoute(method string , pattern string , handler HandlerFunc) { if _, ok := r.roots[method]; !ok { r.roots[method] = &node{children: make (map [string ]*node)} } r.roots[method].insert(pattern) key := method + "-" + pattern r.handlers[key] = handler }func (r *router) getRouter(method string , pattern string ) (*node, map [string ]string ) { if _, ok := r.roots[method]; !ok { return nil , nil } return r.roots[method].search(pattern) }func (r *router) handle(c *Context) { node, params := r.getRouter(c.Method, c.Path) if node != nil { c.Params = params key := c.Method + "-" + node.path r.handlers[key](c) } else { fmt.Fprintf(c.Writer, "404 NOT FOUND: %s\n" , c.Req.URL) } }
gee/trie.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 package geeimport ( "strings" )type node struct { path string part string children map [string ]*node isWild bool }func (root *node) insert(pattern string ) { cur := root patterns := parsePath(pattern) for _, part := range patterns { if _, ok := cur.children[part]; !ok { cur.children[part] = &node{ part: part, children: make (map [string ]*node), isWild: part[0 ] == ':' || part[0 ] == '*' , } } cur = cur.children[part] } cur.path = pattern }func (root *node) search(pattern string ) (*node, map [string ]string ) { params := make (map [string ]string ) cur := root patterns := parsePath(pattern) for _, part := range patterns { if cur.children[part] == nil { for k, v := range cur.children { if v.isWild == true && k[0 ] == '*' { params[k[1 :]] = pattern[strings.Index(pattern, part):] return v, params } else if v.isWild == true && k[0 ] == ':' { params[k[1 :]] = part cur = v break } else { return nil , nil } } } else { cur = cur.children[part] } } if cur.path != "" { return cur, params } return nil , nil }func parsePath (pattern string ) []string { patterns := strings.Split(pattern, "/" ) if len (patterns) > 0 && patterns[0 ] == "" { patterns = patterns[1 :] } if len (patterns) > 0 && patterns[len (patterns)-1 ] == "" { patterns = patterns[:len (patterns)-1 ] } return patterns }
main.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 package mainimport ( "net/http" "version_3_router/gee" )func main () { r := gee.New() r.GET("/hello" , func (c *gee.Context) { c.String(http.StatusOK, "hello %s, you're at %s\n" , c.Query("name" ), c.Path) }) r.GET("/hello/:name" , func (c *gee.Context) { c.String(http.StatusOK, "hello %s, you're at %s\n" , c.Param("name" ), c.Path) }) r.GET("/assets/*filepath" , func (c *gee.Context) { c.JSON(http.StatusOK, gee.H{"filepath" : c.Param("filepath" )}) }) r.Run(":9999" ) }
4.分组控制 为什么需要路由分组 便于统一处理某一组路由。比如这类需求,
以/post
开头的路由匿名可访问。 以/admin
开头的路由需要鉴权。 以/api
开头的路由是 RESTful 接口,可以对接第三方平台,需要三方平台鉴权。 这里补充两点:
常见的路由分组方式,就是以相同前缀来区分不同的路由。 如何统一处理某一组路由,需要使用中间件,下文第五章会详细叙述。 如何设计路由分组功能 以相同前缀来区分不同的路由。我们先看下,用户想如何使用,
1 2 3 4 5 6 r := gee.New() v1 := r.Group("/v1" ) user := v1.Group("/user" ) user.GET("/hello" , Hello)
为了实现上述功能,需要做两件事:
首先,我们使用 RouterGroup 来保存分组的数据,比如前缀、作用在这个分组上的中间件。
其次,RouterGroup 需要增加路由的能力(GET、POST),我们使用 组合(Composite) 来实现,在 RouterGroup 的肚子里,放一个全局的 Engine 指针变量。
1 2 3 4 type RouterGroup struct { prefix string engine *Engine }
通过该指针变量,间接使用 Engine 的各种能力,比如封装自己的 GET、POST 方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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 }func (group *RouterGroup) GET(pattern string , handler HandlerFunc) { pattern = group.prefix + pattern group.engine.GET(pattern, handler) }func (group *RouterGroup) POST(pattern string , handler HandlerFunc) { pattern = group.prefix + pattern group.engine.POST(pattern, handler) }
为什么将 Engine 抽象为最顶层的 RouterGroup 上述方式可以达到目的,但是我们会发现一个问题,
Engine 可以通过 GET/POST 方法增加路由,RouterGroup 也需要。 Engine 可以通过 中间件 增加请求处理行为,RouterGroup 也需要。 在这两个功能上,Engine 和 RouterGroup 是一样的,没必要写两套同样的逻辑。可以将 Engine 抽象成最顶层的 RouterGroup。
如何将 Engine 抽象为最顶层的 RouterGroup 需要做两件事情:
将这上述的两个功能(增加路由、使用中间件)全交给 RouterGroup 来做。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 func (group *RouterGroup) addRoute(method string , pattern string , handler HandlerFunc) { pattern = group.prefix + pattern group.engine.router.addRoute(method, pattern, handler) }func (group *RouterGroup) GET(pattern string , handler HandlerFunc) { group.addRoute("GET" , pattern, handler) }func (group *RouterGroup) POST(pattern string , handler HandlerFunc) { group.addRoute("POST" , pattern, handler) }
通过 嵌入(Embeding)的方式,让 Engine 拥有 RouterGroup 全部能力。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 type Engine struct { *RouterGroup router *router groups []*RouterGroup }func New () *Engine { engine := &Engine{router: newRouter()} engine.RouterGroup = &RouterGroup{engine: engine} engine.groups = []*RouterGroup{engine.RouterGroup} return engine }
为什么要使用 嵌入的方式 当把路由能力交给 routerGroup来完成后,Engine 可以隐性地使用 RouterGroup 的方法。
1 2 3 engine.GET("hello" , hello)
完整代码 1 2 3 4 5 6 7 8 9 10 version_4_group ├── gee │ ├── context.go │ ├── gee.go │ ├── router.go │ └── trie.go ├── go.mod └── main.go
gee/gee.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 package geeimport ( "net/http" )type HandlerFunc func (ctx *Context) type H map [string ]interface {}type Engine struct { *RouterGroup router *router groups []*RouterGroup }func New () *Engine { engine := &Engine{router: newRouter()} engine.RouterGroup = &RouterGroup{engine: engine} engine.groups = []*RouterGroup{engine.RouterGroup} return engine }func (engine *Engine) Run(addr string ) error { return http.ListenAndServe(addr, engine) }func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { context := NewContext(w, req) engine.router.handle(context) }type RouterGroup struct { prefix string middleware []HandlerFunc 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 }func (group *RouterGroup) addRoute(method string , pattern string , handler HandlerFunc) { pattern = group.prefix + pattern group.engine.router.addRoute(method, pattern, handler) }func (group *RouterGroup) GET(pattern string , handler HandlerFunc) { group.addRoute("GET" , pattern, handler) }func (group *RouterGroup) POST(pattern string , handler HandlerFunc) { group.addRoute("POST" , pattern, handler) }
main.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 package mainimport ( "net/http" "version_4_group/gee" )func main () { engine := gee.New() v1 := engine.Group("/v1" ) v1.GET("/" , func (ctx *gee.Context) { ctx.HTML(http.StatusOK, "<h1>hello Gee</h1>" ) }) v1.GET("/hello" , func (ctx *gee.Context) { ctx.String(http.StatusOK, "hello %s" , ctx.Query("name" )) }) v1.POST("/login" , func (ctx *gee.Context) { ctx.JSON(http.StatusOK, gee.H{ "username" : ctx.PostForm("username" ), "password" : ctx.PostForm("password" ), }) }) err := engine.Run(":9999" ) if err != nil { panic (err) } }
5.中间件 为什么需要中间件 可以在不入侵业务逻辑的情况下,在业务开始前/结束后完成一些功能,比如 logger、recover。web 框架需要提供一个插入点,允许用户自定义功能。
中间件的执行顺序 例如 group v1、user 分别有中间件 middlewareA,middlewareB。则 /v1/user/hello 需要执行两个中间件,middlewareA、middlewareB。
执行的过程,和函数调用栈是一样的,
middlewareA 前半部分(middlewareA 入栈,执行) middlewareB 前半部分(middlewareB 入栈,执行) 真正的请求处理函数 handle(handle 入栈,执行,出栈) middlewareB 后半部分(middlewareB 继续执行,出栈) middlewareA 后半部分(middlewareA 继续执行,出栈) 如何设计中间件 用户会如何使用?
用户传入一个 func(ctx *gee.Context)
函数作为中间件。 中间件分为三部分,上半部分、执行下一个中间件/请求处理函数、下班部分。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 r := gee.New() v1 := r.Group("/v1" ) v1.Use(func (ctx *gee.Context) { now := time.Now() ctx.Next() fmt.Println(time.Since(now)) })
为了实现上述功能,需要做三件事:
储存中间件。 处理请求时,明确该执行哪些中件间。 实现一种机制,让中间件依次执行。 下面依次说明。
储存中间件。 首先中间件的类型是 func(ctx *gee.Context)
,也就是 HandlerFunc 类型。
既然中间件和 RouterGroup 绑定的,我们就在 RouterGroup 中保存一个 HandlerFunc 数组,用来储存这个分组下的中间件。(一个分组可能有多个中间件)
1 2 3 4 5 6 7 8 9 10 11 type RouterGroup struct { prefix string middlewares []HandlerFunc engine *Engine }func (group *RouterGroup) Use(handlerFunc ...HandlerFunc) { group.middlewares = append (group.middlewares, handlerFunc...) }
处理请求时,明确该执行哪些中间件。 思路:每个 RouterGroup 都保存了各自的中间件,我们在处理请求时,查看该请求,属于哪些分组,并将这些分组的中间件,均保存下来,交给 context 依次执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { var middlewares []HandlerFunc for _, group := range engine.groups { prefix := group.prefix + "/" if strings.HasPrefix(req.URL.Path, prefix) { middlewares = append (middlewares, group.middlewares...) } } context := NewContext(w, req) context.handlers = middlewares engine.router.handle(context) }
实现一种机制,让中间件依次执行。 上一步提到,将保存下来的中间件,交给 context 依次执行。一个小疑问,为什么要交给 context 执行?
中间件也是 HandlerFunc 类型,可以和请求处理函数统一起来,让请求处理函数跟在中间件后面,按顺序执行。 context 信息相当于全局信息,将 HandlerFunc 数组放在 context 中,无论执行哪个函数,均可知道下一个该执行哪个函数。(用 index 记录执行到哪个函数) 所以我们在 context 中新建两个变量 handlers 和 index。将 index 初始化为 -1,表示还没开始执行函数,下一个执行 handlers[0]。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 type Context struct { Req *http.Request Writer http.ResponseWriter Path string Method string Params map [string ]string StatusCode int handlers []HandlerFunc index int }func NewContext (writer http.ResponseWriter, req *http.Request) *Context { return &Context{ Req: req, Writer: writer, Path: req.URL.Path, Method: req.Method, index: -1 , } }
并提供执行下一个中间件/请求处理函数的方法。
1 2 3 4 5 6 7 8 9 func (c *Context) Next() { c.index++ s := len (c.handlers) for ; c.index < s; c.index++ { c.handlers[c.index](c) } }
准备完毕,最后需要让 context.handlers 执行。
原本逻辑是 ServeHTTP -> router.handle 执行 请求处理函数,现在需要把请求处理函数也加在context.handlers 后面,一并执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 func (r *router) handle(c *Context) { node, params := r.getRouter(c.Method, c.Path) if node != nil { c.Params = params key := c.Method + "-" + node.path c.handlers = append (c.handlers, r.handlers[key]) } else { fmt.Fprintf(c.Writer, "404 NOT FOUND: %s\n" , c.Req.URL) } c.Next() }
为什么需要遍历 handlers 1 2 3 4 5 6 7 8 9 10 11 12 13 14 func (c *Context) Next() { c.index++ s := len (c.handlers) for ; c.index < s; c.index++ { c.handlers[c.index](c) } }func (c *Context) Next() { c.index++ c.handlers[c.index](c) }
不是所有的 handler 都会调用 Next()
。手动调用 Next()
,一般用于在请求前后各实现一些行为。如果中间件只作用于请求前,写法一可以省略调用Next()
,兼容性更好。
Gin 中是如何设计中间件的 和本文思路一致,区别在于 “明确该执行哪些中间件” 这一步,
本文是执行请求时,才开始查找要执行哪些中间件。在 ServeHTTP 中遍历所有 RouterGroup。 Gin 是在 addRouter 时,将中间件保存在 Router 的 map 中。 主要看三个关键代码:
**RouterGroup.Group。**每个中间件都保存该组执行的所有中间件。 1 2 3 4 5 6 7 8 9 10 11 v1 := engine.Group("v1" ) v1.Use(FuncA) user := v1.Group("user" ) user.Use(FuncB)
Gin 如何实现这一步骤?
1 2 3 4 5 6 7 8 9 func (group *RouterGroup) Group(relativePath string , handlers ...HandlerFunc) *RouterGroup { return &RouterGroup{ Handlers: group.combineHandlers(handlers), basePath: group.calculateAbsolutePath(relativePath), engine: group.engine, } }
**RouterGroup.GET -> RouterGroup.handle。**合并路径,合并中间件和请求处理函数到同一队列中。 1 2 3 4 5 6 7 8 func (group *RouterGroup) handle(httpMethod, relativePath string , handlers HandlersChain) IRoutes { absolutePath := group.calculateAbsolutePath(relativePath) handlers = group.combineHandlers(handlers) group.engine.addRoute(httpMethod, absolutePath, handlers) return group.returnObj() }
**RouterGroup.handle -> RouterGroup.combineHandlers。**合并中间件和请求处理函数到同一队列中的具体实现。(该函数也用于合并当前组的中间件和上层 RouterGroup 的中间件,原理一样。) 1 2 3 4 5 6 7 8 9 10 func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain { finalSize := len (group.Handlers) + len (handlers) assert1(finalSize < int (abortIndex), "too many handlers" ) mergedHandlers := make (HandlersChain, finalSize) copy (mergedHandlers, group.Handlers) copy (mergedHandlers[len (group.Handlers):], handlers) return mergedHandlers }
完整代码 1 2 3 4 5 6 7 8 9 10 version_5_middleware ├── gee │ ├── context.go │ ├── gee.go │ ├── router.go │ └── trie.go ├── go.mod └── main.go
gee/gee.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 package geeimport ( "net/http" "strings" )type HandlerFunc func (ctx *Context) type H map [string ]interface {}type Engine struct { *RouterGroup router *router groups []*RouterGroup }func New () *Engine { engine := &Engine{router: newRouter()} engine.RouterGroup = &RouterGroup{engine: engine} engine.groups = []*RouterGroup{engine.RouterGroup} return engine }func (engine *Engine) Run(addr string ) error { return http.ListenAndServe(addr, engine) }func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { var middlewares []HandlerFunc for _, group := range engine.groups { prefix := group.prefix + "/" if strings.HasPrefix(req.URL.Path, prefix) { middlewares = append (middlewares, group.middlewares...) } } context := NewContext(w, req) context.handlers = middlewares engine.router.handle(context) }type RouterGroup struct { prefix string middlewares []HandlerFunc 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 }func (group *RouterGroup) addRoute(method string , pattern string , handler HandlerFunc) { pattern = group.prefix + pattern group.engine.router.addRoute(method, pattern, handler) }func (group *RouterGroup) GET(pattern string , handler HandlerFunc) { group.addRoute("GET" , pattern, handler) }func (group *RouterGroup) POST(pattern string , handler HandlerFunc) { group.addRoute("POST" , pattern, handler) }func (group *RouterGroup) Use(handlerFunc ...HandlerFunc) { group.middlewares = append (group.middlewares, handlerFunc...) }
gee/context.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 package geeimport ( "encoding/json" "fmt" "net/http" )type Context struct { Req *http.Request Writer http.ResponseWriter Path string Method string Params map [string ]string StatusCode int handlers []HandlerFunc index int }func NewContext (writer http.ResponseWriter, req *http.Request) *Context { return &Context{ Req: req, Writer: writer, Path: req.URL.Path, Method: req.Method, index: -1 , } }func (c *Context) Next() { c.index++ s := len (c.handlers) for ; c.index < s; c.index++ { c.handlers[c.index](c) } }func (c *Context) PostForm(key string ) string { return c.Req.FormValue(key) }func (c *Context) Query(key string ) string { return c.Req.URL.Query().Get(key) }func (c *Context) Param(key string ) string { return c.Params[key] }func (c *Context) SetHeader(key string , value string ) { c.Writer.Header().Set(key, value) }func (c *Context) Status(code int ) { c.StatusCode = code c.Writer.WriteHeader(code) }func (c *Context) String(code int , format string , values ...interface {}) { c.SetHeader("Content-Type" , "text/plain" ) c.Status(code) if _, err := c.Writer.Write([]byte (fmt.Sprintf(format, values...))); err != nil { http.Error(c.Writer, err.Error(), 500 ) } }func (c *Context) JSON(code int , obj interface {}) { c.SetHeader("Context-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 ) } }func (c *Context) Data(code int , date []byte ) { c.Status(code) if _, err := c.Writer.Write(date); err != nil { http.Error(c.Writer, err.Error(), 500 ) } }func (c *Context) HTML(code int , html string ) { c.SetHeader("Context-Type" , "text/html" ) c.Status(code) if _, err := c.Writer.Write([]byte (html)); err != nil { http.Error(c.Writer, err.Error(), 500 ) } }
gee/router.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 package geeimport "fmt" type router struct { roots map [string ]*node handlers map [string ]HandlerFunc }func newRouter () *router { return &router{ roots: make (map [string ]*node), handlers: make (map [string ]HandlerFunc), } }func (r *router) addRoute(method string , pattern string , handler HandlerFunc) { if _, ok := r.roots[method]; !ok { r.roots[method] = &node{children: make (map [string ]*node)} } r.roots[method].insert(pattern) key := method + "-" + pattern r.handlers[key] = handler fmt.Println("key" , key) }func (r *router) getRouter(method string , pattern string ) (*node, map [string ]string ) { if _, ok := r.roots[method]; !ok { return nil , nil } return r.roots[method].search(pattern) }func (r *router) handle(c *Context) { node, params := r.getRouter(c.Method, c.Path) if node != nil { c.Params = params key := c.Method + "-" + node.path c.handlers = append (c.handlers, r.handlers[key]) } else { fmt.Fprintf(c.Writer, "404 NOT FOUND: %s\n" , c.Req.URL) } c.Next() }
main.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 package mainimport ( "fmt" "net/http" "time" "version_5_middleware/gee" )func main () { r := gee.New() v1 := r.Group("/v1" ) v1.Use(func (ctx *gee.Context) { now := time.Now() ctx.Next() fmt.Println(time.Since(now)) }) user := v1.Group("/user" ) user.GET("/hello/:name" , func (ctx *gee.Context) { ctx.JSON(http.StatusOK, gee.H{ "name" : ctx.Param("name" ), }) }) err := r.Run(":9999" ) if err != nil { panic (err) } }
6.模版