文章目录
- 将路由(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.Request
和 http.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. 优势
- 代码最开头,给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"),
})
Context
目前只包含了http.ResponseWriter
和*http.Request
,另外提供了对Method
和Path
这两个常用属性的直接访问。- 提供了访问
Query
和PostForm
参数的方法。 - 提供了快速构造
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文件中呢?
这样做会导致:
- 职责不够明确:router.go 文件可能会包含过多的接口定义,职责不够单一。
- 在编程中,"接口定义"通常指的是对外提供的功能或方法,这些方法是供其他模块或用户调用的。在Go语言中,接口定义可以是显式的接口类型,也可以是一个结构体的方法集合。
GET
、POST
这些方法可以被视为接口的一部分,因为它们是供用户或其他模块调用的功能。它们定义了如何向路由器添加路由。 - 如果将这些方法全部放在 router.go 文件中,可能会导致以下问题:
- 混合职责:router.go 文件既包含了路由的存储和查找逻辑,又包含了对外提供的接口方法,职责不够单一。
- 文件复杂性:随着功能的增加,router.go 文件可能会变得过于庞大,难以管理。
- 接口暴露:如果 router.go 文件中包含了过多的接口方法,可能会暴露一些不必要的实现细节,降低代码的封装性。
- handle 方法也可以被视为接口的一部分,但它的作用和使用场景与
GET
、POST
等方法有所不同。- 内部接口:
handle
方法主要用于框架内部的请求处理流程。它根据请求的路径和方法查找对应的处理函数并执行。这是路由器的核心功能之一。 - 封装性:将
handle
方法放在 router.go 中,可以将路由查找和处理的逻辑封装在路由器内部,保持框架接口的简洁性。
- 内部接口:
- 在编程中,"接口定义"通常指的是对外提供的功能或方法,这些方法是供其他模块或用户调用的。在Go语言中,接口定义可以是显式的接口类型,也可以是一个结构体的方法集合。
- 接口暴露:可能会暴露一些不必要的实现细节给用户,降低接口的简洁性。