目 录CONTENT

文章目录
Go

02.跟着Geek兔兔学习-Go语言动手写Web框架-设计&添加上下文Context

Hello!你好!我是村望~!
2023-09-01 / 0 评论 / 0 点赞 / 295 阅读 / 2,191 字
温馨提示:
我不想探寻任何东西的意义,我只享受当下思考的快乐~

设计&添加上下文Context

感谢Geek 兔的教程

示例代码,解释摘抄 https://geektutu.com/ + 结合自己的理解

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

使用时的效果

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

.
├── gee
│   ├── context.go
│   ├── gee.go
│   └── go.mod
├── go.mod
└── main.go

标准库的问题

对Web服务来说,无非是根据请求*http.Request,构造响应http.ResponseWriter

但是在标准库中,这两个对象封装度太低。

我们要构造一个完整的响应,需要考虑:消息头(Header)和消息体(Body)

  • 而 Header 包含了状态码(StatusCode),

  • 消息类型(ContentType。JSON or text/html. …ext )等几乎每次请求都需要设置的信息。

因此,如果不进行有效的封装,那么框架的用户将需要写大量重复,繁杂的代码,而且容易出错。

针对常用场景,能够高效地构造出 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)
}

VS 封装后:

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

当然了,封装*http.Requesthttp.ResponseWriter的方法,简化相关接口的调用,只是设计 Context 的原因之一

对于框架来说,还需要支撑额外的功能。例如,将来解析动态路由/hello/:name,参数:name的值放在哪呢?[我们希望可以从context中取出来]

再比如,框架需要支持中间件,那中间件产生的信息放在哪呢?[我们也希望能从Context中取出来]

Context 随着每一个请求的出现而产生,请求的结束而销毁,和当前请求强相关的信息都应由 Context 承载。

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

package gee

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

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
}

// newContext Context构造器函数
func newContext(w http.ResponseWriter, req *http.Request) *Context {
	return &Context{
		Writer: w,
		Req:    req,
		Path:   req.URL.Path,
		Method: req.Method,
	}
}

// PostForm 支持 form 参数获取
func (c *Context) PostForm(key string) string {
	return c.Req.FormValue(key)
}

// Query 支持 Query 参数获取
func (c *Context) Query(key string) string {
	return c.Req.URL.Query().Get(key)
}

// Status 支持写入header 状态码
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...)))
}

// 响应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)
	}
}

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

// byte数据
func (c *Context) Data(code int, data []byte) {
	c.Status(code)
	c.Writer.Write(data)
}
  • 代码最开头,给map[string]interface{}起了一个别名gee.H,构建JSON数据时,显得更简洁。
  • Context目前只包含了http.ResponseWriter*http.Request,另外提供了对 Method 和 Path 这两个常用属性的直接访问。
  • 提供了访问Query和PostForm参数的方法。
  • 提供了快速构造String/Data/JSON/HTML响应的方法。

路由(Router)

我们将和路由相关的方法和结构提取了出来,放到了一个新的文件中router.go

.
├── gee
│   ├── context.go
│   ├── gee.go
│   ├── go.mod
│   └── router.go
├── go.mod
└── main.go

2 directories, 6 files

方便我们下一次对 router 的功能进行增强,例如提供动态路由的支持。

router 的 handle 方法作了一个细微的调整,即 handler 的参数,变成了 Context。

package gee

import (
	"log"
	"net/http"
)

type HandlerFunc func(*Context)

type router struct {
	handlers map[string]HandlerFunc
}

func newRouter() *router {
	return &router{handlers: make(map[string]HandlerFunc)}
}
// addRoute 路由表添加key 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
}

// handle 统一调用handleFunc
func (r *router) handle(c *Context) {
	key := c.Method + "-" + c.Path
	if handler, ok := r.handlers[key]; ok {
    // 在路由表中根据 key 查对应的方法,然后执行~
		handler(newContext(c.Writer, c.Req))
	} else {
		c.String(http.StatusNotFound, "404 NOT FOUND: %s\n", c.Path)
	}
}

重构后的gee,go

package gee

import (
	"net/http"
)

// 实现ServeHTTP接口
type Engine struct {
	// 路由表 like: GET-/hello -> HandlerFunc
	router router
}

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

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

// POST 定义添加POST请求的方法
func (engine *Engine) POST(pattern string, handler HandlerFunc) {
	engine.router.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 使用

package main

import (
	"gee"
	"net/http"
)

func main() {
	r := gee.New()
	r.GET("/", func(ctx *gee.Context) {
		ctx.JSON(http.StatusOK, gee.H{
			"main": "ok",
		})
	})

	r.GET("/index.html", func(ctx *gee.Context) {
		ctx.HTML(http.StatusOK, "<div style='color:red'>Hello<div>")
	})

	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.GET("/data", func(ctx *gee.Context) {
		ctx.Data(http.StatusOK, []byte{96, 97, 98, 99})
	})
	r.Run(":9999")
}

  1. Engine 使用 router addRoute 方法 注册Method+路由Path->HandlerFunc到路由表
  2. Engine 内部实现 ServeHTTP方法:内部 router handle方法router.handle(c) 根据key := c.Method + "-" + c.Path 获取key 查询对应的HandlerFun,并执行
  3. 所有的相关信息全部挂载到Context 中
❯ curl "http://localhost:9999/data"❯ curl -i http://localhost:9999/
HTTP/1.1 200 OK
Content-Type: application/json
Date: Fri, 01 Sep 2023 08:34:29 GMT
Content-Length: 14

{"main":"ok"}


❯ curl "http://localhost:9999/hello?name=cunwangbro"
hello cunwangbro, you're at /hello


❯ curl "http://localhost:9999/login" -X POST -d 'username=gee&password=1234'
{"password":"1234","username":"gee"}

❯ curl "http://localhost:9999/data"
`abc%                 

image-20230901163640789

总结一下

1⃣️.加入了 Context 其实是对 handler 部分的封装和扩展!

封装了原始的字段

  • Writer http.ResponseWriter
  • Req *http.Request

因为这里还是用 map 做路由表,想方便的取到 Method和URL.Path 所以多了 Method和Path 的字段

  • Path string
  • Method string

2⃣️.想从Context 直接获取参数,所以封装了 :

  • func (c *Context) PostForm(key string) string
  • func (c *Context) Query(key string) string

想通过 Context 直接返回响应所以封装了:

  • func (c *Context) String(code int, format string, values ...interface{})
  • func (c *Context) JSON(code int, obj interface{})
  • func (c *Context) HTML(code int, html string)
  • func (c *Context) Data(code int, data []byte)

3⃣️.注册路由的方式,还是和之前差不多~ 唯一的变化就是handler的参数是 Context

参数的取出,以及返回的响应都通过Context做到!

我们实现的 func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) 内部就是根据当前请求构造一个Context,然后传递给handler去处理!

0

评论区