【极客兔兔-Web框架Gee详解】Day2 上下文Context


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

一、框架结构

├─day1-http_base
│  ├─base1
│  │      main.go
│  │
│  ├─base2
│  │      main.go
│  │
│  └─base3
│      │  go.mod
│      │
│      └─gee
│              go.mod
│
└─day2-context
    │  go.mod
    │  main.go
    │
    └─gee
            context.go
            gee.go
            go.mod
            router.go

在这里插入图片描述
初始化模块:在Terminal中
在这里插入图片描述

二、设计上下文(Context):day2-context/gee/context.go

1. 设计Context必要性

Web服务来说,无非是根据请求*http.Request,构造响应http.ResponseWriter。
Web 服务开发中,处理 HTTP 请求和响应是核心任务。Go 语言的标准库提供了 *http.Requesthttp.ResponseWriter 来处理请求和响应,但直接使用它们可能导致以下问题:

1.1 接口粒度过细:

  • 构造一个完整的 HTTP 响应需要设置许多细节,如消息头(Header)和消息体(Body)。消息头中包含状态码(StatusCode)、内容类型(ContentType)等信息,这些几乎每次请求都需要设置。
  • 如果不进行封装,开发者需要重复编写大量代码来处理这些细节,容易出错。
  • 针对常用场景,能够高效地构造出 HTTP 响应是一个好的框架必须考虑的点。

1.2 缺乏扩展性:

  • 标准库的接口无法直接支持一些高级功能。例如,动态路由 /hello/:name 中的参数 :name 如何提取和存储?
  • 中间件需要传递和存储信息,这些信息应该放在哪里?
  • Context 随着每一个请求的出现而产生,请求的结束而销毁,和当前请求强相关的信息都应由 Context 承载。

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

2. 代码

package gee

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

// H 是一个简化的 map 类型,用于 JSON 数据
type H map[string]interface{}

// Context 封装了 HTTP 请求和响应的上下文
type Context struct {
	Writer http.ResponseWriter // HTTP 响应写入器
	Req    *http.Request       // HTTP 请求
	// 请求信息
	Path   string // 请求路径
	Method string // 请求方法
	// 响应信息
	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 获取 HTTP 请求的POST 表单数据
func (c *Context) PostForm(key string) string {
	// 从 HTTP 请求中获取指定 key 的表单值,这是 Go 的 http.Request 提供的方法,用于获取 POST 表单数据。
	return c.Req.FormValue(key)
}

// Query 获取 URL 查询参数
func (c *Context) Query(key string) string {
	// 从 URL 查询字符串中获取指定 key 的值, Go 的 http.Request 提供的方法,用于获取 URL 查询参数。
	return c.Req.URL.Query().Get(key)
}

// Status 设置 HTTP 响应状态码, 这是个封装方法。
func (c *Context) Status(code int) {
	c.StatusCode = code
	//这是一个低级别的操作,用于指示请求的处理结果(例如,200 表示成功,404 表示未找到,500 表示服务器错误等)。
	//WriteHeader 只能调用一次,并且必须在写入响应体之前调用。
	c.Writer.WriteHeader(code)
}

// SetHeader 设置 HTTP 响应头,这是一个封装方法,简化了设置响应头的过程。它内部调用了 c.Writer.Header().Set(key, value)。
func (c *Context) SetHeader(key string, value string) {
	//设置 HTTP 响应头的键值对。
	//用于设置响应的元数据,例如 Content-Type、Content-Length、Set-Cookie 等。
	//可以在 WriteHeader 调用之前多次设置。
	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)
	// 创建一个 JSON 编码器,将响应写入 c.Writer
	encoder := json.NewEncoder(c.Writer)
	// 尝试将 obj 编码为 JSON 格式并写入响应
	if err := encoder.Encode(obj); err != nil {
		// 如果编码失败,返回 500 状态码和错误信息
		http.Error(c.Writer, err.Error(), 500)
	}
}

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

3. 优势

  1. 代码最开头,给map[string]interface{}起了一个别名gee.H,构建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"),
})
  1. Context目前只包含了http.ResponseWriter*http.Request,另外提供了对 MethodPath 这两个常用属性的直接访问。
  2. 提供了访问QueryPostForm参数的方法。
  3. 提供了快速构造String/Data/JSON/HTML响应的方法。

三、路由(Router): day2-context/gee/router.go

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

package gee

import (
	"log"
	"net/http"
)

// router 结构体用于存储路由信息
type router struct {
	handlers map[string]HandlerFunc // 存储路由和处理函数的映射
}

// newRouter 创建一个新的 router 实例
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 {
		handler(c) // 如果找到处理函数,调用它
	} else {
		c.String(http.StatusNotFound, "404 NOT FOUND: %s\n", c.Path) // 否则返回 404 错误
	}
}

四、框架入口:day2-context/gee/gee.go

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

1. 代码

package gee

import "net/http"

// HandlerFunc 定义了请求处理函数的类型
type HandlerFunc func(*Context)

// Engine 是框架的核心结构,包含一个路由器
type Engine struct {
	router *router // 路由器,用于管理路由和处理请求
}

// New 创建一个新的 Engine 实例
func New() *Engine {
	return &Engine{router: newRouter()} // 初始化路由器
}

// addRoute 添加路由到路由器
func (engine *Engine) addRoute(method string, pattern string, handler HandlerFunc) {
	engine.router.addRoute(method, pattern, handler) // 调用路由器的 addRoute 方法
}

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

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

// Run 启动 HTTP 服务器
func (engine *Engine) Run(addr string) (err error) {
	return http.ListenAndServe(addr, engine) // 监听并服务 HTTP 请求
}

// ServeHTTP 实现 http.Handler 接口,处理所有的 HTTP 请求
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	c := newContext(w, req)         // 创建新的请求上下文
	engine.router.handle(c)         // 通过路由器处理请求
}

五、框架使用: day2-context/main.go

1. 代码

package main

import (
	"gee"      // 导入自定义的 gee 包
	"net/http" // 导入 net/http 包,用于 HTTP 状态码
)

func main() {
	r := gee.New() // 创建一个新的 gee.Engine 实例

	// 定义一个 GET 请求的路由,路径为 "/"
	r.GET("/", func(c *gee.Context) {
		// 返回一个 HTML 响应,状态码为 200
		c.HTML(http.StatusOK, "<h1>Hello Gee</h1>")
	})

	// 定义一个 GET 请求的路由,路径为 "/hello"
	r.GET("/hello", func(c *gee.Context) {
		// 期望请求格式为 /hello?name=geektutu
		// 返回一个格式化的字符串响应,包含查询参数 "name" 和请求路径
		c.String(http.StatusOK, "hello %s, you're at %s\n", c.Query("name"), c.Path)
	})

	// 定义一个 POST 请求的路由,路径为 "/login"
	r.POST("/login", func(c *gee.Context) {
		// 返回一个 JSON 响应,包含从 POST 表单中获取的 "username" 和 "password"
		c.JSON(http.StatusOK, gee.H{
			"username": c.PostForm("username"),
			"password": c.PostForm("password"),
		})
	})

	// 启动 HTTP 服务器,监听端口 9999
	r.Run(":9999")
}

2. 处理流程

  • 当一个 HTTP 请求到达时,ServeHTTP 方法被调用。
  • 它首先创建一个 Context,将请求和响应的相关信息封装起来。
  • 然后,它调用路由器的 handle 方法,传入 Context。
  • 路由器根据请求的路径和方法查找并执行相应的处理函数。
  • 处理函数使用 Context 来生成响应,设置状态码、响应头,并写入响应体。

六、运行结果

运行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=geektutu"
hello geektutu, you're at /hello

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

$ curl "http://localhost:9999/xxx"
404 NOT FOUND: /xxx

七、扩展

1. 为什么不把和路由相关的POST、GET等方法放到路由文件router.go中,handle(c *Context)执行处理方法放到框架入口gee.go文件中呢?

这样做会导致:

  1. 职责不够明确:router.go 文件可能会包含过多的接口定义,职责不够单一。
    • 在编程中,"接口定义"通常指的是对外提供的功能或方法,这些方法是供其他模块或用户调用的。在Go语言中,接口定义可以是显式的接口类型,也可以是一个结构体的方法集合。GETPOST 这些方法可以被视为接口的一部分,因为它们是供用户或其他模块调用的功能。它们定义了如何向路由器添加路由。
    • 如果将这些方法全部放在 router.go 文件中,可能会导致以下问题:
      • 混合职责:router.go 文件既包含了路由的存储和查找逻辑,又包含了对外提供的接口方法,职责不够单一。
      • 文件复杂性:随着功能的增加,router.go 文件可能会变得过于庞大,难以管理。
      • 接口暴露:如果 router.go 文件中包含了过多的接口方法,可能会暴露一些不必要的实现细节,降低代码的封装性。
    • handle 方法也可以被视为接口的一部分,但它的作用和使用场景与 GETPOST 等方法有所不同。
      • 内部接口:handle 方法主要用于框架内部的请求处理流程。它根据请求的路径和方法查找对应的处理函数并执行。这是路由器的核心功能之一。
      • 封装性:将 handle 方法放在 router.go 中,可以将路由查找和处理的逻辑封装在路由器内部,保持框架接口的简洁性。
  2. 接口暴露:可能会暴露一些不必要的实现细节给用户,降低接口的简洁性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

再坚持一下嘤

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值