前言

在手写框架的时候,遇到了极多的问题,由于我是在参考这个项目模仿Gin框架,关于Gin的一些设计还有不少要理解的路要走。对于我这个框架GoMatrix,对他的期望就是简洁明了,就像python中的falsk一样。不过最终成品如何,只能看后续了。

Context

如果是学习过Gin框架的童鞋,看到这个上下文一定感到分外的亲切。是的,目前没有自己的想法,只能顺着Gin框架把这个上下文抄下来了。如果像上一篇文章那样,每一个函数都要根据请求*http.Request,构造响应http.ResponseWriter,但是这两个对象提供的接口粒度太细,比如我们要构造一个完整的响应,需要考虑消息头(Header)和消息体(Body),而 Header 包含了状态码(StatusCode),消息类型(ContentType)等几乎每次请求都需要设置的信息。

如果不进行有效的封装,那么框架的用户将需要写大量重复繁琐的代码,因此,能够简化相关接口调用,高效的构造出一个HTTP响应是一个框架应该考虑的点。

而且,我们都知道在内置的http包中,我们无法解析动态路由,比如:/hello/:name:name的值放在哪儿呢。再比如中间件巴拉巴拉的,Context 随着每一个请求的出现而产生,请求的结束而销毁,和当前请求强相关的信息都应由 Context 承载。因此,设计 Context 结构,扩展性和复杂性留在了内部,而对外简化了接口。路由的处理函数,以及将要实现的中间件,参数都统一使用 Context 实例, Context 就像一次会话的百宝箱,可以找到任何东西。

新建一个文件context.go:

package GoMatrix

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

type H map[string]interface{}

type Context struct {
	// 原对象
	Writer http.ResponseWriter
	Req    *http.Request
	// 请求信息
	Path   string
	Method string
	// 响应信息
	StatusCode int
}

// 生成新的上下文
func newContext(w http.ResponseWriter, req *http.Request) *Context {
	return &Context{
		Writer: w,
		Req:    req,
		Path:   req.URL.Path,
		Method: req.Method,
	}
}

// 访问Form参数的方法

func (c *Context) PostForm(key string) string {
	return c.Req.FormValue(key)
}

// 访问Query参数的方法

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

// 构造string响应

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

// 构造文件响应

func (c *Context) Data(code int, data []byte) {
	c.Status(code)
	c.Writer.Write(data)
}

// 构造HTML响应

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

重点说下Data响应,因为返回的是个数据流,之前在使用Gin的时候,踩过一个坑,就是在使用某个文件服务的时候,他没给返回文件的content-type,这就导致了一个问题,无法判断该数据流的类型,而在使用Gin的c.Data时,contentType又是一个必填项,这就造成了,如果该文件是pdf或者word,亦或者只是image/png,我就只能选其一。

不过这也是极个别情况,因为正常文件服务一定会有contentType,我也只是倒霉的碰上一个没有的罢了。

PS:我最后的解决办法是,使用了Gin上下文的c.Writer.Write方法,直接绕过了他的Data

抽离router

路由的设计是一个不小的工程,如果全挤在一个入口文件中,也是非常的不好看,而且修改也是很不易,所以直接将其在早期抽离,我也不能确定这个框架的代码量,有没有必要抽离成文件夹,后期如果实在不方便再抽离也行。

新建router.go

// 抽离router
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) {
	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)
	}
}

同时,入口文件也要进行更改,将原来的原生http方法改成我们的上下文。

GoMatrix.go

// HandlerFunc定义使用的请求处理程序,替换成上下文

type HandlerFunc func(c *Context)

// 引擎实现ServeHTTP接口

type Engine struct {
	router *router
}

// 初始化引擎

func New() *Engine {
	return &Engine{router: newRouter()}
}


func (engine *Engine) addRoute(method string, pattern string, handler HandlerFunc) {
	log.Printf("Route %4s - %s", method, pattern)
	engine.router.addRoute(method, pattern, handler)
}

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

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

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

func (engine *Engine)DELETE(pattern string, handler HandlerFunc) {
	engine.addRoute("DELETE", pattern, handler)
}

// 定义启动http服务器的方法
func (engine *Engine) Run(addr string) (err error) {
	fmt.Printf("ListenAndServe %s \n", addr)
	return http.ListenAndServe(addr, engine)
}

// 实现Handler接口
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	c := newContext(w, req)
	engine.router.handle(c)
}

小总结

目前的上下文还是非常简单的,只是将其原来的对象包含进去,抽离了路由之后,文件的层次分的还算不错了。在调用 router.handle 之前,构造了一个 Context 对象,通过实现了 ServeHTTP 接口,接管了所有的 HTTP 请求,理解了Gin的一些底层原理,也是非常具有实质性进展的。

Unfinished to be continued…