day2 上下文Context

本文代码地址:day2 上下文Context

本文是 7天用Go从零实现Web框架Gee教程系列的第二篇。

主要内容如下:

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

动手写 Gee 框架的第二天,框架代码140行,新增代码约90行

使用效果

为了展示第二天的成果,以及加强对要实现的功能的整体感知,我们首先看一看在使用时的效果。

day2-context/main.go

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

必要性
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)
}

在此处补充一点json.NewEncoder的小知识

json.NewEncoder Go 语言中标准库encoding/json包中的一个函数,用于创建一个新的 JSON 编码器。通过调用 json.NewEncoder(),你可以得到一个 JSON 编码器对象,然后可以使用它将 Go 结构体、映射等数据结构编码为JSON 格式的字符串。

以下是一个简单的示例,展示了如何使用 json.NewEncoder 创建编码器并进行编码:

package main

import (
    "encoding/json"
    "fmt"
)

type Person struct {
    Name string
    Age  int
}

func main() {
    // 创建 Person 结构体的实例
    p := Person{Name: "Alice", Age: 25}

    // 创建 JSON 编码器
    encoder := json.NewEncoder(os.Stdout)

    // 将 Person 结构体编码为 JSON
    err := encoder.Encode(p)
    if err!= nil {
        fmt.Println("编码错误:", err)
        return
    }
}

在上述示例中,首先定义了一个名为 Person 的结构体,它具有 NameAge 字段。然后在 main 函数中,创建了一个 Person 结构体的实例 p

接下来,使用json.NewEncoder(os.Stdout)创建一个将输出发送到标准输出的 JSON 编码器。通过调用 encoder.Encode(p),将 p 编码为 JSON 并打印到标准输出。

请注意,调用encoder.Encode(p)进行编码为JSON时,需要进行错误检查,以确保编码过程中没有发生错误。如果有错误发生,你可以根据需要进行相应的处理。

这只是一个简单的示例,实际上,你可以根据自己的需求将编码后的 JSON 字符串保存到文件、网络传输等。

VS 封装后:

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

Context具体实现

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

day2-context/gee/context.go

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
}

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

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

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,另外提供了对MethodPath这两个常用属性的直接访问,方便后续快速取用。

  • 提供了访问QueryPostForm参数的方法。

  • 提供了快速构造String/Data/JSON/HTML响应的方法。

路由(Router)

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

day2-context/gee/router.go

type router struct {
	handlers map[string]HandlerFunc
}

func newRouter() *router {
	return &router{handlers: make(map[string]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
}

func (r *router) handle(c *Context) {
	key := c.Method + "-" + c.Path
	if handler, ok := r.handlers[key]; ok {
		// 注意:接收的参数由w http.ResponseWriter, r *http.Request变为了*Context
		handler(c)
	} else {
		c.String(http.StatusNotFound, "404 NOT FOUND: %s\n", c.Path)
	}
}

框架入口

day2-context/gee/gee.go

// HandlerFunc defines the request handler used by gee
// 接收的参数由w http.ResponseWriter, r *http.Request变为了*Context
type HandlerFunc func(*Context)

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

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

func (engine *Engine) addRoute(method string, 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)
}

// Run defines the method to start a http server
func (engine *Engine) Run(addr string) (err error) {
	return http.ListenAndServe(addr, engine)
}

func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	// 构造了一个Context对象
	c := newContext(w, req)
	engine.router.handle(c)
}

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

从上面的代码不难看出,用户使用的时候是直接用Engine对象,低啊用Engine的相关方法,及时是和路由相关的也是直接使用Engine调用,但是Engine的路由底层实现其实是委托给了router对象的。

如何使用,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=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

原文地址:https://geektutu.com/post/gee-day2.html

  • 18
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
SQLAlchemy 是一个 SQL 工具包和对象关系映射(ORM)库,用于 Python 编程语言。它提供了一个高级的 SQL 工具和对象关系映射工具,允许开发者以 Python 类和对象的形式操作数据库,而无需编写大量的 SQL 语句。SQLAlchemy 建立在 DBAPI 之上,支持多种数据库后端,如 SQLite, MySQL, PostgreSQL 等。 SQLAlchemy 的核心功能: 对象关系映射(ORM): SQLAlchemy 允许开发者使用 Python 类来表示数据库表,使用类的实例表示表中的行。 开发者可以定义类之间的关系(如一对多、多对多),SQLAlchemy 会自动处理这些关系在数据库中的映射。 通过 ORM,开发者可以像操作 Python 对象一样操作数据库,这大大简化了数据库操作的复杂性。 表达式语言: SQLAlchemy 提供了一个丰富的 SQL 表达式语言,允许开发者以 Python 表达式的方式编写复杂的 SQL 查询。 表达式语言提供了对 SQL 语句的灵活控制,同时保持了代码的可读性和可维护性。 数据库引擎和连接池: SQLAlchemy 支持多种数据库后端,并且为每种后端提供了对应的数据库引擎。 它还提供了连接池管理功能,以优化数据库连接的创建、使用和释放。 会话管理: SQLAlchemy 使用会话(Session)来管理对象的持久化状态。 会话提供了一个工作单元(unit of work)和身份映射(identity map)的概念,使得对象的状态管理和查询更加高效。 事件系统: SQLAlchemy 提供了一个事件系统,允许开发者在 ORM 的各个生命周期阶段插入自定义的钩子函数。 这使得开发者可以在对象加载、修改、删除等操作时执行额外的逻辑。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值