title: Go语言动手写Web框架 - Gee第二天 上下文Context
date: 2019-08-19 00:10:10
description: 7天用 Go语言 从零实现Web框架教程(7 days implement golang web framework from scratch tutorial),用 Go语言/golang 动手写Web框架,从零实现一个Web框架,以 Gin 为原型从零设计一个Web框架。本文介绍了请求上下文(Context)的设计理念,封装了返回JSON/String/Data/HTML等类型响应的方法。
tags:

  • Go
    nav: 从零实现
    categories:
  • Web框架 - Gee
    keywords:
  • Go语言
  • 从零实现Web框架
  • 动手写Web框架
  • Context
    image: post/gee/gee.jpg
    github: https://github.com/geektutu/7days-golang
    book: 七天用Go从零实现系列
    book_title: Day2 上下文

本文是 7天用Go从零实现Web框架Gee教程系列的第二篇。

  • 路由(router)独立出来,方便之后增强。
  • 设计上下文(Context),封装 Request 和 Response ,提供对 JSON、HTML 等返回类型的支持。
  • 动手写 Gee 框架的第二天,框架代码140行,新增代码约90行

使用效果

为了展示第二天的成果,我们看一看在使用时的效果。

day2-context/main.go

  1. func main() {
  2. r := gee.New()
  3. r.GET("/", func(c *gee.Context) {
  4. c.HTML(http.StatusOK, "<h1>Hello Gee</h1>")
  5. })
  6. r.GET("/hello", func(c *gee.Context) {
  7. // expect /hello?name=geektutu
  8. c.String(http.StatusOK, "hello %s, you're at %s\n", c.Query("name"), c.Path)
  9. })
  10. r.POST("/login", func(c *gee.Context) {
  11. c.JSON(http.StatusOK, gee.H{
  12. "username": c.PostForm("username"),
  13. "password": c.PostForm("password"),
  14. })
  15. })
  16. r.Run(":9999")
  17. }
  • Handler的参数变成成了gee.Context,提供了查询Query/PostForm参数的功能。
  • gee.Context封装了HTML/String/JSON函数,能够快速构造HTTP响应。

设计Context

必要性

  1. 对Web服务来说,无非是根据请求*http.Request,构造响应http.ResponseWriter。但是这两个对象提供的接口粒度太细,比如我们要构造一个完整的响应,需要考虑消息头(Header)和消息体(Body),而 Header 包含了状态码(StatusCode),消息类型(ContentType)等几乎每次请求都需要设置的信息。因此,如果不进行有效的封装,那么框架的用户将需要写大量重复,繁杂的代码,而且容易出错。针对常用场景,能够高效地构造出 HTTP 响应是一个好的框架必须考虑的点。

用返回 JSON 数据作比较,感受下封装前后的差距。

封装前

  1. obj = map[string]interface{}{
  2. "name": "geektutu",
  3. "password": "1234",
  4. }
  5. w.Header().Set("Content-Type", "application/json")
  6. w.WriteHeader(http.StatusOK)
  7. encoder := json.NewEncoder(w)
  8. if err := encoder.Encode(obj); err != nil {
  9. http.Error(w, err.Error(), 500)
  10. }

VS 封装后:

  1. c.JSON(http.StatusOK, gee.H{
  2. "username": c.PostForm("username"),
  3. "password": c.PostForm("password"),
  4. })
  1. 针对使用场景,封装*http.Requesthttp.ResponseWriter的方法,简化相关接口的调用,只是设计 Context 的原因之一。对于框架来说,还需要支撑额外的功能。例如,将来解析动态路由/hello/:name,参数:name的值放在哪呢?再比如,框架需要支持中间件,那中间件产生的信息放在哪呢?Context 随着每一个请求的出现而产生,请求的结束而销毁,和当前请求强相关的信息都应由 Context 承载。因此,设计 Context 结构,扩展性和复杂性留在了内部,而对外简化了接口。路由的处理函数,以及将要实现的中间件,参数都统一使用 Context 实例, Context 就像一次会话的百宝箱,可以找到任何东西。

具体实现

day2-context/gee/context.go

  1. type H map[string]interface{}
  2. type Context struct {
  3. // origin objects
  4. Writer http.ResponseWriter
  5. Req *http.Request
  6. // request info
  7. Path string
  8. Method string
  9. // response info
  10. StatusCode int
  11. }
  12. func newContext(w http.ResponseWriter, req *http.Request) *Context {
  13. return &Context{
  14. Writer: w,
  15. Req: req,
  16. Path: req.URL.Path,
  17. Method: req.Method,
  18. }
  19. }
  20. func (c *Context) PostForm(key string) string {
  21. return c.Req.FormValue(key)
  22. }
  23. func (c *Context) Query(key string) string {
  24. return c.Req.URL.Query().Get(key)
  25. }
  26. func (c *Context) Status(code int) {
  27. c.StatusCode = code
  28. c.Writer.WriteHeader(code)
  29. }
  30. func (c *Context) SetHeader(key string, value string) {
  31. c.Writer.Header().Set(key, value)
  32. }
  33. func (c *Context) String(code int, format string, values ...interface{}) {
  34. c.SetHeader("Content-Type", "text/plain")
  35. c.Status(code)
  36. c.Writer.Write([]byte(fmt.Sprintf(format, values...)))
  37. }
  38. func (c *Context) JSON(code int, obj interface{}) {
  39. c.SetHeader("Content-Type", "application/json")
  40. c.Status(code)
  41. encoder := json.NewEncoder(c.Writer)
  42. if err := encoder.Encode(obj); err != nil {
  43. http.Error(c.Writer, err.Error(), 500)
  44. }
  45. }
  46. func (c *Context) Data(code int, data []byte) {
  47. c.Status(code)
  48. c.Writer.Write(data)
  49. }
  50. func (c *Context) HTML(code int, html string) {
  51. c.SetHeader("Content-Type", "text/html")
  52. c.Status(code)
  53. c.Writer.Write([]byte(html))
  54. }
  • 代码最开头,给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. type router struct {
  2. handlers map[string]HandlerFunc
  3. }
  4. func newRouter() *router {
  5. return &router{handlers: make(map[string]HandlerFunc)}
  6. }
  7. func (r *router) addRoute(method string, pattern string, handler HandlerFunc) {
  8. log.Printf("Route %4s - %s", method, pattern)
  9. key := method + "-" + pattern
  10. r.handlers[key] = handler
  11. }
  12. func (r *router) handle(c *Context) {
  13. key := c.Method + "-" + c.Path
  14. if handler, ok := r.handlers[key]; ok {
  15. handler(c)
  16. } else {
  17. c.String(http.StatusNotFound, "404 NOT FOUND: %s\n", c.Path)
  18. }
  19. }

框架入口

day2-context/gee/gee.go

  1. // HandlerFunc defines the request handler used by gee
  2. type HandlerFunc func(*Context)
  3. // Engine implement the interface of ServeHTTP
  4. type Engine struct {
  5. router *router
  6. }
  7. // New is the constructor of gee.Engine
  8. func New() *Engine {
  9. return &Engine{router: newRouter()}
  10. }
  11. func (engine *Engine) addRoute(method string, pattern string, handler HandlerFunc) {
  12. engine.router.addRoute(method, pattern, handler)
  13. }
  14. // GET defines the method to add GET request
  15. func (engine *Engine) GET(pattern string, handler HandlerFunc) {
  16. engine.addRoute("GET", pattern, handler)
  17. }
  18. // POST defines the method to add POST request
  19. func (engine *Engine) POST(pattern string, handler HandlerFunc) {
  20. engine.addRoute("POST", pattern, handler)
  21. }
  22. // Run defines the method to start a http server
  23. func (engine *Engine) Run(addr string) (err error) {
  24. return http.ListenAndServe(addr, engine)
  25. }
  26. func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
  27. c := newContext(w, req)
  28. engine.router.handle(c)
  29. }

router相关的代码独立后,gee.go简单了不少。最重要的还是通过实现了 ServeHTTP 接口,接管了所有的 HTTP 请求。相比第一天的代码,这个方法也有细微的调整,在调用 router.handle 之前,构造了一个 Context 对象。这个对象目前还非常简单,仅仅是包装了原来的两个参数,之后我们会慢慢地给Context插上翅膀。

如何使用,main.go一开始就已经亮相了。运行go run main.go,借助 curl ,一起看一看今天的成果吧。

  1. $ curl -i http://localhost:9999/
  2. HTTP/1.1 200 OK
  3. Date: Mon, 12 Aug 2019 16:52:52 GMT
  4. Content-Length: 18
  5. Content-Type: text/html; charset=utf-8
  6. <h1>Hello Gee</h1>
  7. $ curl "http://localhost:9999/hello?name=geektutu"
  8. hello geektutu, you're at /hello
  9. $ curl "http://localhost:9999/login" -X POST -d 'username=geektutu&password=1234'
  10. {"password":"1234","username":"geektutu"}
  11. $ curl "http://localhost:9999/xxx"
  12. 404 NOT FOUND: /xxx