这是搭建 Gee Web 框架的第二天
本节主要内容:
- 将
路由(router)
独立出来,方便之后增强。
- 设计
上下文(Context)
,封装 Request 和 Response ,提供对 JSON、HTML 等返回类型的支持。
- 动手写 Gee 框架的第二天,框架代码140行,新增代码约90行
使用效果展示
为了展示第二天的成果,我们看一看在使用时的效果。
day2-context/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 main
import ( "net/http"
"gee" )
func 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) { 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") }
|
Handler
的参数变成成了 gee.Context
,提供了查询 Query/PostForm 参数的功能。
gee.Context
封装了 HTML/String/JSON
函数,能够快速构造 HTTP 响应。
设计Context
必要性
1.对 Web 服务来说,无非是根据 *http.Request
,构造响应 http.ResponseWriter
。但是这两个对象提供的接口粒度太低,比如我们要构造一个完整的响应,需要考虑消息头(Header)和消息体(Body),而 Header 包含了状态码(StatusCode),消息类型(ContentType)等几乎每次从请求都需要设置的信息。因此,如果不进行有效的封装,那么框架的用户需要写大量重复,繁杂的代码,而且容易出错。争对常用场景,能够高效地构造出 HTTP 响应是一个好的框架必须考虑的点。所以本节最重要的点就是对路由 router
的封装。
用汉辉的 JSON 数据做比较,感受下封装前后的差距。
封装前
1 2 3 4 5 6 7 8 9 10
| obj = map[string]interface{}{ "name": "geektutu", "password": "1234", } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) encoder := json.NewEncoder(w) if err := encoder.Encode(obj); err != nil { http.Error(w, err.Error(), 500) }
|
VS 封装后:
1 2 3 4
| c.JSON(http.StatusOK, gee.H{ "username": c.PostForm("username"), "password": c.PostForm("password"), })
|
2.争对使用场景,封装 *http.Request
和 http.ResponseWriter
的方法,简化相关接口的调用,这只是使用上下文 Context 的原因之一,对于整个框架而言,它哈需要支撑额外的功能。例如,将来解析动态路由 /hello/:name
,参数 :name
放在哪里等。再比如,框架需要支持中间件,那么中间件产生的信息又要放在哪里呢? Context 随着每一个请求的出现而产生,请求的结束而销毁 ,和当前请求强相关的信息丢应由 Context 承载。因此,设计 Context 结构,扩展性和复杂性留在内部,而对外简化了接口。路由的处理函数,以及将要实现的中间件,参数都统一使用 Context
实例,Conetext
就像一次会话的百宝箱,可以找到任何东西。
具体实现
day2-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
| package gee
import ( "encoding/json" "fmt" "net/http" )
type H map[string]interface{}
type Context struct { Writer http.ResponseWriter Req *http.Request Path string Method string StatueCode int }
func newContext(w http.ResponseWriter, req *http.Request) *Context { return &Context{ Writer: w, Req: req, 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) Status(code int) { c.StatueCode = code c.Writer.WriteHeader(code) }
func (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) } }
func (c *Context) Data(code int, data []byte) { c.Status(code) c.Writer.Write(data) }
func (c *Context) HTML(code int, html string) { c.SetHeader("Content-Type", "text/html") c.Status(code) c.Writer.Write([]byte(html)) }
|
- 代码最开头,给
map[string]interface{}
起了一个别名gee.H
,构建 JSON 数据时,显得更简洁。
Context
目前只包含了http.ResponseWriter
和*http.Request
,另外提供了对 Method 和 Path 这两个常用属性的直接访问。
- 提供了访问Query和PostForm参数的方法。
- 提供了快速构造String/Data/JSON/HTML响应的方法。
路由(Router)
我们将和路由相关的方法和结构提取了出来,放到了一个新的文件中 router.go
,方便我们下一次对 router 的功能进行增强,例如提供动态路由的支持。 router 的 handle 方法作了一个细微的调整,即 handler 的参数,变成了 Context
。我们将和路由相关的方法和结构提取了出来,放到了一个新的文件中router.go
,方便我们下一次对 router 的功能进行增强,例如提供动态路由的支持。 router 的 handle 方法作了一个细微的调整,即 handler 的参数,变成了 Context
。
day2-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
| package gee
import ( "log" "net/http" )
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) { log.Printf("Route %4s - %s", method, pattern) key := method + "-" + pattern r.handlers[key] = handler }
func (r *router) handle(c *Context) { key := c.Method + "-" + c.Path if handler, ok := r.handlers[key]; ok { handler(c) } else { c.String(http.StatusNotFound, "404 NOT FOUND: %s\n", c.Path) } }
|
框架入口
day2-context/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
| package gee
import ( "net/http" )
type HandlerFunc func(*Context)
type Engine struct { router *router }
func New() *Engine { return &Engine{router: newRouter()} }
func (engine *Engine) addRoute(method string, pattern string, handler HandlerFunc) { engine.router.addRoute(method, pattern, 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) (err error) { return http.ListenAndServe(addr, engine) }
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { c := newContext(w, req) engine.router.handle(c) }
|
将 router
相关的代码独立后,gee.go
简单了不少。最重要的还是通过实现了 ServeHTTP 接口,接管了所有的 HTTP 请求。相比第一天的代码,这个方法也有细微的调整,在调用 router.handle 之前,构造了一个 Context 对象。这个对象目前还非常简单,仅仅是包装了原来的两个参数,之后我们会慢慢地给 Context 插上翅膀。
如何使用,main.go
一开始就已经亮相了。运行go build main.go
,借助 postman ,一起看一看今天的成果吧。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| $ http://localhost:9999/ HTTP/1.1 200 OK Date: Wed, 23 Nov 2022 13:13:42 GMT Content-Length: 18 Content-Type: text/html; charset=utf-8 <h1>Hello Gee</h1>
$ http://localhost:9999/hello?name=wuxi hello wuxi, you're at /hello
$ http://localhost:9999/login" -X POST -d 'username=wuxi&password=123' {"password":"123","username":"wuxi"}
$ http://localhost:9999/xxx 404 NOT FOUND: /xxx
|
整理了一下思路,整个框架实现如下
参考:
7days-golang/gee-web/day2-context/gee at master · geektutu/7days-golang (github.com)