上下文Context

目标

  • 路由(router)独立出来,方便之后增强。
  • 设计上下文(Context),封装 Request 和 Response ,提供对 JSON、HTML 等返回类型的支持。

设计Context

  • Web服务是根据请求*http.Request,构造响应http.ResponseWriter

  • 要构造一个完整的响应,需要考虑消息头(Header)和消息体(Body),而 Header 包含了状态码(StatusCode),消息类型(ContentType)等几乎每次请求都需要设置的信息。

  • 若不进行有效的封装,那么框架的用户将需要写大量重复,繁杂的代码,而且容易出错。针对常用场景,能够高效地构造出 HTTP 响应是一个好的框架必须考虑的点。

栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 封装之前
obj = map[string]interface{}{
"name": "geektutu",
"password": "1234",
}
w.Header().Set("Content-Type", "application/json") //设置 http response 响应头
w.WriteHeader(http.StatusOK)
encoder := json.NewEncoder(w) // 编码的一个实例
if err := encoder.Encode(obj); err != nil { // 编码
http.Error(w, err.Error(), 500)
}

// 封装之后
c.JSON(http.StatusOK, gee.H{
"username": c.PostForm("username"),
"password": c.PostForm("password"),
})
  • Context 随着每一个请求的出现而产生,请求的结束而销毁,和当前请求强相关的信息都应由 Context 承载。
  • 设计 Context 结构,扩展性和复杂性留在了内部,而对外简化了接口。路由的处理函数,以及将要实现的中间件,参数都统一使用 Context 实例, Context 就像一次会话的百宝箱,可以找到任何东西。

具体实现

项目结构

1
2
3
4
5
6
7
gee/
|--gee.go
|--context.go
|--router.go
|--go.mod
main.go
go.mod

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 gee

import (
"encoding/json"
"fmt"
"net/http"
)

// H 给map[string]interface{}起了一个别名gee.H 用来存放各种响应数据gin中源码也是类似
type H map[string]interface{}

type Context struct {
// origin objects
Writer http.ResponseWriter
Req *http.Request
// 请求信息
Path string
Method string
// 响应信息
StatusCode int
}

// 返回一个Context实例
func newContext(w http.ResponseWriter, req *http.Request) *Context {
return &Context{
Writer: w,
Req: req,
Path: req.URL.Path,
Method: req.Method,
}
}

// PostForm 获取Query和PostForm参数的方法。
func (c *Context) PostForm(key string) string {
return c.Req.FormValue(key)
}

// Query 获取Query和PostForm参数的方法。
func (c *Context) Query(key string) string {
return c.Req.URL.Query().Get(key)
}

// Status 设置响应头状态码
func (c *Context) Status(code int) {
c.StatusCode = code
c.Writer.WriteHeader(code)
}

// SetHeader 设置响应头返回类型
func (c *Context) SetHeader(key string, value string) {
c.Writer.Header().Set(key, value)
}

// 构造String响应方法
func (c *Context) String(code int, format string, value ...interface{}) {
c.SetHeader("Content-Type", "text/plain")
c.Status(code)
c.Writer.Write([]byte(fmt.Sprintf(format, value)))
}

// JSON 构造JSON响应方法
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)
}
}

// Data 构造Data响应方法
func (c *Context) Data(code int, data []byte) {
c.Status(code)
c.Writer.Write(data)
}

// HTML 构造HTML响应方法
func (c *Context) HTML(code int, html string) {
c.SetHeader("Content-Type", "text/html")
c.Status(code)
c.Writer.Write([]byte(html))
}

router.go

路由:将和路由相关的方法和结构提取了出来,放到了一个新的文件中router.go,方便我们下一次对 router 的功能进行增强,例如提供动态路由的支持。

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
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)
}
}

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
package gee

import "net/http"

// HandlerFunc 定义gee使用的请求处理程序
type HandlerFunc func(*Context)

// Engine 实现了ServeHTTP接口
type Engine struct {
router *router
}

// New 是gee.Engine的构造函数
func New() *Engine {
return &Engine{router: newRouter()}
}

// 将路由和处理方法注册到映射表 *router* 中
func (engine *Engine) addRoute(method string, pattern string, handler HandlerFunc) {
engine.router.addRoute(method, pattern, handler)
}

// GET 定义添加GET请求的方法
func (engine *Engine) GET(pattern string, handler HandlerFunc) {
engine.addRoute("GET", pattern, handler)
}

// POST 定义添加POST请求的方法
func (engine *Engine) POST(pattern string, handler HandlerFunc) {
engine.addRoute("POST", pattern, handler)
}

// Run 定义启动HTTP服务的方法
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)
}

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
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(":9090")
}

总结

  1. router相关的代码独立。
  2. 通过实现了 ServeHTTP 接口,接管了所有的 HTTP 请求。
  3. 在调用 router.handle 之前,构造了一个 Context 对象。