本文通过回答关键问题的方式,记录阅读 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[geeweb] ├── 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" "geeweb/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[geeweb] ├── 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" "geeweb/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[geeweb] ├── 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" "geeweb/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[geeweb] ├── 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" "geeweb/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[geeweb] ├── 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" "geeweb/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.模版 模板可以干什么 同一个 HTML 模板,样式是一样的,但是根据后端返回的数据不同,页面展示的数据不同。
举个例子,用户个人中心页面,虽然页面布局大致相同,但是不同用户的页面上显示的用户名是不同的。这就是根据后端返回的不同数据,基于同一个模板来渲染的。
如何实现 HTML 渲染 Go语言内置了text/template
和html/template
2个模板标准库,其中 html/template 为 HTML 提供了较为完整的支持。包括普通变量渲染、列表渲染、对象渲染等。
我们需要做的就是,将 html/template 封装为 Engine 的能力。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 type Engine struct { *RouterGroup router *router groups []*RouterGroup htmlTemplates *template.Template funcMap template.FuncMap }func (engine *Engine) SetFuncMap(funcMap template.FuncMap) { engine.funcMap = funcMap }func (engine *Engine) LoadHTMLGlob(pattern string ) { engine.htmlTemplates = template.New("" ) engine.htmlTemplates.Funcs(engine.funcMap) engine.htmlTemplates = template.Must(engine.htmlTemplates.ParseGlob(pattern)) }
最后在 context 中,重新改造 HTML 函数。让 htmlTemplates 来响应请求。
1 2 3 4 5 6 7 8 9 10 11 12 func (c *Context) HTML(code int , name string , data interface {}) { c.SetHeader("Context-Type" , "text/html" ) c.Status(code) err := c.engine.htmlTemplates.ExecuteTemplate(c.Writer, name, data) if err != nil { c.Fail(500 , err.Error()) } }
模板格式如下,其中,{{.title}}
表示 title 变量填充在这个位置,{{.now | FormatAsDate}}
表示 now 变量数据经过 FormatAsDate 函数处理之后填充在这个位置。
最后,main 函数中设置自定义函数、传入 gee.H。
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 func FormatAsDate (t time.Time) string { year, month, day := t.Date() return fmt.Sprintf("%d-%02d-%02d" , year, month, day) }func main () { r := gee.New() r.SetFuncMap(template.FuncMap{ "FormatAsDate" : FormatAsDate, }) r.LoadHTMLGlob("templates/*" ) r.GET("/date" , func (c *gee.Context) { c.HTML(http.StatusOK, "custom_func.tmpl" , gee.H{ "title" : "gee" , "now" : time.Date(2019 , 8 , 17 , 0 , 0 , 0 , 0 , time.UTC), }) }) r.Run(":9999" ) }
如何在模板中使用静态文件 在渲染 html 文件的同时,还会需要加载一些静态文件,比如 css、js 文件。
需要提供给用户 Static 方法,使用 Static 方法配置完成后,用户访问 localhost:9999/assets/css/index.css,最终返回 /usr/static/css/index.css。
1 2 3 r.Static("/assets" , "/usr/static/" )
Static 方法需要做什么工作?
通过拼接得到路由路径 “/assets/*filepath”。 创建路由处理函数 handler。 添加路由映射 RouterGroup.GET。 1 2 3 4 5 6 7 8 func (group *RouterGroup) Static(relativePath string , root string ) { urlPattern := path.Join(relativePath, "/*filepath" ) handler := group.createStaticHandler(relativePath, http.Dir(root)) group.GET(urlPattern, handler) }
这个 handler 应该具有什么能力?
先去掉所有前缀,只剩下文件路径。例如 localhost:9999/assets/css/index.css 去掉前缀后是,css/index.css 在 /usr/static/ 的目录下,建立文件系统,在文件系统中打开 css/index.css。 其中,第一点,去掉所有路径可以由 http.StripPrefix 完成;第二点,建立文件系统打开文件可以由 http.FileServer 完成。我们只需要封装一个这样的 handler。
如何封装?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 func (group *RouterGroup) createStaticHandler(relativePath string , fs http.FileSystem) HandlerFunc { absolutePath := path.Join(group.prefix, relativePath) fileServer := http.StripPrefix(absolutePath, http.FileServer(fs)) return func (ctx *Context) { file := ctx.Param("filepath" ) if _, err := fs.Open(file); err != nil { ctx.Status(http.StatusNotFound) return } fileServer.ServeHTTP(ctx.Writer, ctx.Req) } }
有趣的函数式编程 功能实现后,进一步看原码,会发现这里函数式编程很有意思。
首先, 明确流程
http.StripPrefix 返回一个 HandlerFunc 类型函数 到 handler 接口类型变量 fileServer 中, 当下文调用 fileServer.ServeHTTP 时,就会执行这个 handler。(http.StripPrefix 是包装作用,统一处理错误 也使用了这种用法)
为什么?因为 fileServer 底层是 HandlerFunc 类型,HandlerFunc 实现了 ServeHTTP 函数,调用 ServeHTTP 时就是在调用自己 HandlerFunc。这里讲过:如何接管-HTTP-请求
其次, fileServer 这个 handle 做什么工作?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 func StripPrefix (prefix string , h Handler) Handler { if prefix == "" { return h } return HandlerFunc(func (w ResponseWriter, r *Request) { p := strings.TrimPrefix(r.URL.Path, prefix) rp := strings.TrimPrefix(r.URL.RawPath, prefix) if len (p) < len (r.URL.Path) && (r.URL.RawPath == "" || len (rp) < len (r.URL.RawPath)) { r2 := new (Request) *r2 = *r r2.URL = new (url.URL) *r2.URL = *r.URL r2.URL.Path = p r2.URL.RawPath = rp h.ServeHTTP(w, r2) } else { NotFound(w, r) } }) }
最后,http.FileServer(fs) 这个 handler 做什么工作?主要是打开文件,http.FileServer(fs) 底层是 fileHandler 类型,打开文件的具体实现在 fileHandler 的 ServeHTTP 方法里。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 func FileServer (root FileSystem) Handler { return &fileHandler{root} }func (f *fileHandler) ServeHTTP(w ResponseWriter, r *Request) { upath := r.URL.Path if !strings.HasPrefix(upath, "/" ) { upath = "/" + upath r.URL.Path = upath } serveFile(w, r, f.root, path.Clean(upath), true ) }
完整代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 version_6_template[geeweb] ├── gee │ ├── context.go │ ├── gee.go │ ├── router.go │ └── trie.go ├── static │ └── css │ └── index.css ├── templates │ ├── array.tmpl │ ├── css.tmpl │ └── custom_func.tmpl ├── 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 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 package geeimport ( "fmt" "html/template" "net/http" "time" "geeweb/gee" )type HandlerFunc func (ctx *Context) type H map [string ]interface {}type Engine struct { *RouterGroup router *router groups []*RouterGroup htmlTemplates *template.Template funcMap template.FuncMap }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 context.engine = engine engine.router.handle(context) }func (engine *Engine) SetFuncMap(funcMap template.FuncMap) { engine.funcMap = funcMap }func (engine *Engine) LoadHTMLGlob(pattern string ) { engine.htmlTemplates = template.New("" ) engine.htmlTemplates.Funcs(engine.funcMap) engine.htmlTemplates = template.Must(engine.htmlTemplates.ParseGlob(pattern)) }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...) }func (group *RouterGroup) Static(relativePath string , root string ) { urlPattern := path.Join(relativePath, "/*filepath" ) handler := group.createStaticHandler(relativePath, http.Dir(root)) group.GET(urlPattern, handler) }func (group *RouterGroup) createStaticHandler(relativePath string , fs http.FileSystem) HandlerFunc { absolutePath := path.Join(group.prefix, relativePath) fileServer := http.StripPrefix(absolutePath, http.FileServer(fs)) return func (ctx *Context) { file := ctx.Param("filepath" ) if _, err := fs.Open(file); err != nil { ctx.Status(http.StatusNotFound) return } fileServer.ServeHTTP(ctx.Writer, ctx.Req) } }
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 96 97 98 99 100 101 102 103 104 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 engine *Engine }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) Fail(code int , err string ) { c.index = len (c.handlers) c.JSON(code, H{"message" : err}) }func (c *Context) HTML(code int , name string , data interface {}) { c.SetHeader("Context-Type" , "text/html" ) c.Status(code) err := c.engine.htmlTemplates.ExecuteTemplate(c.Writer, name, data) if err != nil { c.Fail(500 , err.Error()) } }
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 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 package mainimport ( "fmt" "gee/gee" "html/template" "net/http" "time" )type student struct { Name string Age int8 }func FormatAsDate (t time.Time) string { year, month, day := t.Date() return fmt.Sprintf("%d-%02d-%02d" , year, month, day) }func main () { r := gee.New() r.SetFuncMap(template.FuncMap{ "FormatAsDate" : FormatAsDate, }) r.LoadHTMLGlob("templates/*" ) r.Static("/assets" , "./static" ) r.GET("/" , func (c *gee.Context) { c.HTML(http.StatusOK, "css.tmpl" , nil ) }) stu1 := &student{Name: "Aim" , Age: 20 } stu2 := &student{Name: "Tao" , Age: 22 } r.GET("/students" , func (c *gee.Context) { c.HTML(http.StatusOK, "array.tmpl" , gee.H{ "title" : "gee" , "studentArray" : [2 ]*student{stu1, stu2}, }) }) r.GET("/date" , func (c *gee.Context) { c.HTML(http.StatusOK, "custom_func.tmpl" , gee.H{ "title" : "gee" , "now" : time.Date(2019 , 8 , 17 , 0 , 0 , 0 , 0 , time.UTC), }) }) r.Run(":9999" ) }
static/css/index.css
1 2 3 4 5 p { color : orange; font-weight : 700 ; font-size : 20px ; }
templates/array.tmpl
1 2 3 4 5 6 7 8 <html > <body > <p > hello, {{.title}}</p > {{range $index, $ele := .studentArray }} <p > {{ $index }}: {{ $ele.Name }} is {{ $ele.Age }} years old</p > {{ end }}</body > </html >
templates/css.tmpl
1 2 3 4 <html > <link rel ="stylesheet" href ="/assets/css/index.css" > <p > geektutu.css is loaded</p > </html >
templates/custom_func.tmpl
1 2 3 4 5 6 <html > <body > <p > hello, {{.title}}</p > <p > Date: {{.now | FormatAsDate}}</p > </body > </html >
7.错误恢复 为什么需要错误恢复 当处理请求时,出现 panic 等错误会导致整个程序宕掉。为了保证整个程序仍然为其他请求提供服务,就需要进行错误恢复。
如何做错误恢复 1 2 3 4 5 6 func main () { defer fmt.Println("exit" ) fmt.Println("begin" ) panic ("err" ) fmt.Println("end" ) }
上述代码需要明确几点:
出现 panic 错误的位置之后的代码,是无法执行的,程序会直接结束。 在结束之前依旧会执行 defer 程序。 defer 程序就我们错误恢复提供了时机。我们需要在 defer 中,捕获 panic 错误,解析 error 信息,写入 log,并给用户返回 Internal Server Error。
为了保证代码的通用性,我们使用中间件的形式加入 recovery。
1 2 3 4 5 6 7 8 9 10 11 12 13 func Recovery () HandlerFunc { return func (c *Context) { defer func () { if err := recover (); err != nil { log.Printf("%s\n\n" , trace(err)) c.Fail(http.StatusInternalServerError, "Internal Server Error" ) } }() c.Next() } }
中间件中使用 defer 的注意点 中间件不手动调用 Next,函数执行顺序是 中间件开始->中间件结束->路由处理函数开始->路由处理函数结束。
中间件手动调用 Next,函数执行顺序是 中间件开始->路由处理函数开始->路由处理函数结束->中间件结束。
只有手动调用 Next,才能让中间件的 defer 最后执行。举个例子,观察一下 log 输出。
手动调用
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 func main () { r := gee.New() r.Use(Test()) r.GET("/b" , func (ctx *gee.Context) { fmt.Println("b begin." ) ctx.JSON(http.StatusOK, gee.H{"data" : "b" }) fmt.Println("b end." ) }) r.Run(":9999" ) }func Test () gee.HandlerFunc { return func (ctx *gee.Context) { fmt.Println("Test begin." ) defer fmt.Println("Test defer." ) fmt.Println("Test end." ) ctx.Next() } }
不手动调用
不手动调用依然可以执行下一个 HandlerFunc,是因为 Next 函数做了循环。
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 func main () { r := gee.New() r.Use(Test()) r.GET("/b" , func (ctx *gee.Context) { fmt.Println("b begin." ) ctx.JSON(http.StatusOK, gee.H{"data" : "b" }) fmt.Println("b end." ) }) r.Run(":9999" ) }func Test () gee.HandlerFunc { return func (ctx *gee.Context) { fmt.Println("Test begin." ) defer fmt.Println("Test defer." ) fmt.Println("Test end." ) } }
完整代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 version_7_recovery[geeweb] ├── gee │ ├── context.go │ ├── gee.go │ ├── recovery.go │ ├── router.go │ └── trie.go ├── static │ └── css │ └── index.css ├── templates │ ├── array.tmpl │ ├── css.tmpl │ └── custom_func.tmpl ├── go.mod └── main.go
gee/recovery.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 geeimport ( "fmt" "log" "net/http" "runtime" "strings" )func Recovery () HandlerFunc { return func (c *Context) { defer func () { if err := recover (); err != nil { log.Printf("%s\n\n" , trace(err)) c.Fail(http.StatusInternalServerError, "Internal Server Error" ) } }() c.Next() } }func trace (error any) string { message := fmt.Sprintf("%s" , error ) var pcs [32 ]uintptr n := runtime.Callers(3 , pcs[:]) var str strings.Builder str.WriteString(message + "\nTraceback:" ) for _, pc := range pcs[:n] { fn := runtime.FuncForPC(pc) file, line := fn.FileLine(pc) str.WriteString(fmt.Sprintf("\n\t%s:%d" , file, line)) } return str.String() }
main.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package mainimport "geeweb/gee" func main () { r := gee.New() r.Use(gee.Recovery()) r.GET("/" , func (ctx *gee.Context) { panic ("err" ) }) r.Run(":9999" ) }