Go语言动手写Web框架 - Gee第二天 上下文Context
Web框架 - Gee系列文章链接:
7天用Go从零实现Web框架Gee教程
(Aug 11, 2019)
Go语言动手写Web框架 - Gee第一天 http.Handler
(Aug 12, 2019)
Go语言动手写Web框架 - Gee第二天 上下文Context
(Aug 19, 2019)
Go语言动手写Web框架 - Gee第三天 前缀树路由Router
(Aug 28, 2019)
Go语言动手写Web框架 - Gee第四天 分组控制Group
(Sep 1, 2019)
Go语言动手写Web框架 - Gee第五天 中间件Middleware
(Sep 1, 2019)
Go语言动手写Web框架 - Gee第六天 模板(HTML Template)
(Sep 8, 2019)
Go语言动手写Web框架 - Gee第七天 错误恢复(Panic Recover)
(Jan 9, 2020)
源代码/数据集已上传到
Github - 7days-golang
本文是 7天用Go从零实现Web框架Gee教程系列的第二篇。
将路由(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 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) { // expect /hello?name=geektutu c.String(http.StatusOK, "hello %s, you're at %s\n" , c.Query("name" ), c.Path) }) r.POST("/login" , func (c *gee.Context) { c.JSON(http.StatusOK, gee.H{ "username" : c.PostForm("username" ), "password" : c.PostForm("password" ), }) }) r.Run(":9999" ) }
Handler
的参数变成成了gee.Context
,提供了查询Query/PostForm参数的功能。
gee.Context
封装了HTML/String/JSON
函数,能够快速构造HTTP响应。
设计Context 必要性
对Web服务来说,无非是根据请求*http.Request
,构造响应http.ResponseWriter
。但是这两个对象提供的接口粒度太细,比如我们要构造一个完整的响应,需要考虑消息头(Header)和消息体(Body),而 Header 包含了状态码(StatusCode),消息类型(ContentType)等几乎每次请求都需要设置的信息。因此,如果不进行有效的封装,那么框架的用户将需要写大量重复,繁杂的代码,而且容易出错。针对常用场景,能够高效地构造出 HTTP 响应是一个好的框架必须考虑的点。
用返回 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" ), })
针对使用场景,封装*http.Request
和http.ResponseWriter
的方法,简化相关接口的调用,只是设计 Context 的原因之一。对于框架来说,还需要支撑额外的功能。例如,将来解析动态路由/hello/:name
,参数:name
的值放在哪呢?再比如,框架需要支持中间件,那中间件产生的信息放在哪呢?Context 随着每一个请求的出现而产生,请求的结束而销毁,和当前请求强相关的信息都应由 Context 承载。因此,设计 Context 结构,扩展性和复杂性留在了内部,而对外简化了接口。路由的处理函数,以及将要实现的中间件,参数都统一使用 Context 实例, Context 就像一次会话的百宝箱,可以找到任何东西。
具体实现 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 type H map [string ]interface {}type Context struct { // origin objects Writer http.ResponseWriter Req *http.Request // request info Path string Method string // response info StatusCode 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.StatusCode = 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。
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 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 // HandlerFunc defines the request handler used by gee type HandlerFunc func (*Context) // Engine implement the interface of ServeHTTP type Engine struct { router *router } // New is the constructor of gee.Engine func New () *Engine { return &Engine{router: newRouter()} } func (engine *Engine) addRoute (method string , pattern string , handler HandlerFunc) { engine.router.addRoute(method, pattern, handler) } // GET defines the method to add GET request func (engine *Engine) GET (pattern string , handler HandlerFunc) { engine.addRoute("GET" , pattern, handler) } // POST defines the method to add POST request func (engine *Engine) POST (pattern string , handler HandlerFunc) { engine.addRoute("POST" , pattern, handler) } // Run defines the method to start a http server func (engine *Engine) Run (addr string ) (err error) { return http.ListenAndServe(addr, engine) } func (engine *Engine) ServeHTTP (w http.ResponseWriter, req *http.Request) { c := newContext(w, req) engine.router.handle(c) }
将router
相关的代码独立后,gee.go
简单了不少。最重要的还是通过实现了 ServeHTTP 接口,接管了所有的 HTTP 请求。相比第一天的代码,这个方法也有细微的调整,在调用 router.handle 之前,构造了一个 Context 对象。这个对象目前还非常简单,仅仅是包装了原来的两个参数,之后我们会慢慢地给Context插上翅膀。
如何使用,main.go
一开始就已经亮相了。运行go run main.go
,借助 curl ,一起看一看今天的成果吧。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 $ curl -i http://localhost:9999/ HTTP/1.1 200 OK Date: Mon, 12 Aug 2019 16:52:52 GMT Content-Length: 18 Content-Type: text/html; charset=utf-8 <h1>Hello Gee</h1> $ curl "http://localhost:9999/hello?name=geektutu" hello geektutu, you're at /hello $ curl "http://localhost:9999/login" -X POST -d ' username=geektutu&password=1234' {"password":"1234","username":"geektutu"} $ curl "http://localhost:9999/xxx" 404 NOT FOUND: /xxx
last updated at 2022-09-10