【Gee】Day2:上下文

发布于:2025-02-26 ⋅ 阅读:(11) ⋅ 点赞:(0)

Day2:上下文

今天要完成的任务是:

  • 将路由(router)独立出来,方便之后增强(这里说的增强没明白是什么意思);
  • 设计上下文(context),封装 Request 和 Response,提供对 JSON、HTML 等返回类型的支持,代码约 140 行;
    请添加图片描述

使用效果

作者首先展示了第二天的任务完成之后的成果,最显著的变化是,Handler 的参数变味了 gee.Context,而之前是(http.ResponseWriter, *http.Request),说明通过上下文对后二者完成了封装。此外,gee.Context封装了HTML/String/JSON等函数,能够快速构造 HTTP 响应,与 GIN 非常类似。

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

设计 Context

必要性

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

以返回 JSON 为例,感受封装前后的差距:

封装前:

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

封装后:

c.JSON(http.StatusOK, gee.H{
    "username": c.PostForm("username"),
    "password": c.PostForm("password"),
})

此外,针对使用场景,封装*http.Requesthttp.ResponseWriter的方法,简化相关接口的调用,只是设计 Context 的原因之一。对于框架而言,还需要支持额外的功能,比如,解析动态路由/hello/:name时,参数:name的值放在哪里?再比如,框架需要支持中间件(比如 JWT 鉴权中间件),中间件产生的信息放在哪里?Context 随着每个请求的出现而产生,随着请求的结束而销毁,和当前请求强相关的信息都应该由 Context 承载

因此,设计 Context 结构,扩展性和复杂性留在了内部,而对外简化了接口。路由的处理函数,以及将要实现的中间件,参数都统一使用 Context 实例,Context 就像一次会话的百宝箱,从中应该可以找到本次请求我们需要的任何东西。

具体实现

package gee

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

type H map[string]interface{}

type Context struct {
	// origin objects
	// 封装了 http.ResponseWriter 和 *http.Request
	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,
	}
}

// PostForm 方法用于获取 POST 请求中表单字段的值, 它接收一个键名 key, 并返回对应的表单值.
func (c *Context) PostForm(key string) string {
	return c.Req.FormValue(key)
}

// Query 方法用于获取 URL 查询参数的值. 它接收一个键名 key, 并返回对应的查询参数值.
func (c *Context) Query(key string) string {
	return c.Req.URL.Query().Get(key)
}

// Status 方法用于设置 HTTP 响应的状态码. 它接收一个状态码 code, 并将其写入响应头。
func (c *Context) Status(code int) {
	c.StatusCode = code
	c.Writer.WriteHeader(code)
}

// SetHeader 方法用于设置 HTTP 响应头. 它接收一个键值对 key 和 value, 并将其设置为响应头.
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:

package gee

import (
	"log"
	"net/http"
)

type router struct {
	handlers map[string]HandlerFunc
}

// newRouter constructs a router object
func newRouter() *router {
	return &router{handlers: make(map[string]HandlerFunc)}
}

func (r *router) addRoute(method, 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 修改为:

package gee

import (
	"net/http"
)

// HandlerFunc defines the request handler used by gee
type HandlerFunc func(*Context)

// Engine implements the interface of ServeHTTP
type Engine struct {
	router *router
}

// New is the constructor of gee.Engine
func New() *Engine {
	return &Engine{router: newRouter()}
}

// addRoute combines method and pattern together and then add method-pattern and handler to map
func (engine *Engine) addRoute(method, 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)
}

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

可以看到,最明显的区别是,HandlerFunc 类型从处理以http.ResponseWriter以及*http.Request的函数变为了直接处理*Context的函数,这使得框架入口的编写简单了很多。

现在让我们使用更新的 Gee 框架来实现第一天的内容:

package main

import (
	"gee/gee"
	"net/http"
)

func main() {
	r := gee.New()
	r.GET("/", func(c *gee.Context) {
		c.String(http.StatusOK, "URL.Path = %q\n", c.Path)
	})

	r.GET("/hello", func(c *gee.Context) {
		c.JSON(http.StatusOK, c.Req.Header)
	})

	r.Run(":9999")
}

上述代码达到了与第一天类似的效果。