• 从零实现Web框架Geo教程-上下文-02



    本教程参考:七天用Go从零实现Web框架Gee教程


    引言

    本文主要看点;

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

    使用效果:

    func main() {
    	r := geo.New()
    	
    	r.GET("/", func(c *geo.Context) {
    		c.HTML(http.StatusOK, "

    Hello Gee

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

    设计Context

    必要性:

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

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

    封装前:

    obj = map[string]interface{}{
        "name": "dhy",
        "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)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    VS 封装后:

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

    具体实现:

    package geo
    
    import (
    	"encoding/json"
    	"fmt"
    	"net/http"
    )
    
    type H map[string]interface{}
    
    //Context 内部维护当前请求的一系列信息
    type Context struct {
    	//req和res
    	Writer http.ResponseWriter
    	Req    *http.Request
    	//关于请求的相关信息
    	Path   string
    	Method string
    	//关于响应相关信息
    	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)
    }
    
    //Status 设置响应状态码
    func (c *Context) Status(code int) {
    	c.StatusCode = code
    	//如果不手动调用WriteHeader设置响应状态码,那么默认会调用WriteHeader(http.StatusOK)
    	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))
    }
    
    • 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
    • 代码最开头,给map[string]interface{}起了一个别名geo.H,构建JSON数据时,显得更简洁。
    • Context目前只包含了http.ResponseWriter和*http.Request,另外提供了对 Method 和 Path 这两个常用属性的直接访问。
    • 提供了访问Query和PostForm参数的方法。
    • 提供了快速构造String/Data/JSON/HTML响应的方法。

    路由(Router)

    我们将和路由相关的方法和结构提取了出来,放到了一个新的文件中router.go,方便我们下一次对 router 的功能进行增强,例如提供动态路由的支持。 router 的 handle 方法作了一个细微的调整,即 handler 的参数,变成了 Context。

    路由Router的核心方法就是:

    • 增加一个路由映射
    • 获取当前请求对应的路由映射
    package geo
    
    import (
    	"log"
    	"net/http"
    )
    
    type router struct {
    	//路由映射表
    	handlers map[string]HandlerFunc
    }
    
    func newRouter() *router {
    	return &router{handlers: make(map[string]HandlerFunc)}
    }
    
    //addRoute 增加路由,目前就是增加一个路由映射
    func (r *router) addRoute(method string, pattern string, handler HandlerFunc) {
    	log.Printf("Route %4s - %s", method, pattern)
    	key := method + "-" + pattern
    	r.handlers[key] = handler
    }
    
    //handle 从映射表中获取处理器,进行处理
    func (r *router) handle(c *Context) {
    	key := c.Method + "-" + c.Path
    	if handler, ok := r.handlers[key]; ok {
    		//处理器函数入口参数变成了Context
    		handler(c)
    	} else {
    		c.String(http.StatusNotFound, "404 NOT FOUND: %s\n", c.Path)
    	}
    }
    
    • 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

    框架入口

    package geo
    
    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)
    }
    
    • 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

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

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

    $ 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=dhy"
    hello dhy, you're at /hello
    
    $ curl "http://localhost:9999/login" -X POST -d 'username=dhy&password=1234'
    {"password":"1234","username":"dhy"}
    
    $ curl "http://localhost:9999/xxx"
    404 NOT FOUND: /xxx
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    Context#Json Bug分析

    func (c *Context) JSON(code int, obj interface{}) {
    	c.SetHeader("Content-Type", "application/json")
    	c.Status(code)
    	//  c.StatusCode = code
    	//	c.Writer.WriteHeader(code)
    	encoder := json.NewEncoder(c.Writer)
    	if err := encoder.Encode(obj); err != nil {
    		http.Error(c.Writer, err.Error(), 500)
    		//  w.Header().Set("Content-Type", "text/plain; charset=utf-8")
    		//	w.Header().Set("X-Content-Type-Options", "nosniff")
    		//	w.WriteHeader(code)
    		//	fmt.Fprintln(w, error)
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    在 WriteHeader() 后调用 Header().Set 是不会生效的

    如果err!=nil的话http.Error(c.Writer, err.Error(), 500)这里是不起作用的,因为前面已经执行了WriteHeader(code),那么返回码将不会再更改http.Error(c.Writer, err.Error(), 500)里面的w.WriteHeader(code)、w.Header().Set()不起作用,而且encoder.Encode(obj)相当于调用了Write(),http.Error(c.Writer, err.Error(), 500)里面的WriteHeader、Header().Set()操作都是无效的。我看了gin的代码,如果encoder.Encode(obj)这里报错的话是直接panic,感觉这里如果err!=nil的话确实不好处理

    附上gin的代码

    render/json.go 56行

    // Render (JSON) writes data with custom ContentType.
    func (r JSON) Render(w http.ResponseWriter) (err error) {
    	if err = WriteJSON(w, r.Data); err != nil {
    		panic(err)
    	}
    	return
    }
    
    // WriteContentType (JSON) writes JSON ContentType.
    func (r JSON) WriteContentType(w http.ResponseWriter) {
    	writeContentType(w, jsonContentType)
    }
    
    // WriteJSON marshals the given interface object and writes it with custom ContentType.
    func WriteJSON(w http.ResponseWriter, obj interface{}) error {
    	writeContentType(w, jsonContentType)
    	encoder := json.NewEncoder(w)
    	err := encoder.Encode(&obj)
    	return err
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
  • 相关阅读:
    华为机试真题 C++ 实现【连续字母长度】
    重入锁ReentrantLock详解
    搜索系统中的文本相关性实践经验
    字符与字符串
    JWT技术实现用户token令牌管理(9月20号)
    大话超越菜鸟C#的实践入门进阶必知点,深入浅出解析 32 算法入门 循环和递归
    基于残差网络的图像超分
    Linux知识汇总
    cmd运行jar包,txt文件中文乱码问题
    xxl-job 执行成功,但是报“任务结果丢失,标记失败“错误
  • 原文地址:https://blog.csdn.net/m0_53157173/article/details/126558065