拆轮子系列:gin框架

关于WEB框架

gin是go的轻量级的web框架,轻量级意味着仅仅提供web框架应有的基础功能。我觉得看源码最好就是要有目标,看gin这个web框架,我的目标是:

  1. gin这个web框架是怎么实现web框架应有的基础功能的
  2. 代码上实现上有什么值得学习的地方。

一次请求处理的大体流程

如何找到入口

要知道一次请求处理的大体流程,只要找到web框架的入口即可。先看看gin文档当中最简单的demo。Run方法十分耀眼,点击去可以看到关键的http.ListenAndServe,这意味着Engine这个结构体,实现了ServeHTTP这个接口。入口就是Engine实现的ServeHTTP接口。

//我是最简单的demo
func main() {
	r := gin.Default()
	r.GET("/ping", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "pong",
		})
		c.Redirect(http.StatusMovedPermanently, "https://github.com/gin-gonic/gin")

	})
	r.Run() // listen and serve on 0.0.0.0:8080
}

//我是Run方法
func (engine *Engine) Run(addr ...string) (err error) {
	defer func() { debugPrintError(err) }()

	address := resolveAddress(addr)
	debugPrint("Listening and serving HTTP on %s\n", address)
	err = http.ListenAndServe(address, engine)
	return
}

ServeHTTP

大体流程就如注释那样,那么的简单。这里值得关注的是,Context这个上下文对象是在对象池里面取出来的,而不是每次都生成,提高效率。可以看到,真正的核心处理流程是在handleHTTPRequest方法当中。

func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {

	// 从上下文对象池中获取一个上下文对象
	c := engine.pool.Get().(*Context)

	// 初始化上下文对象,因为从对象池取出来的数据,有脏数据,故要初始化。
	c.writermem.reset(w)
	c.Request = req
	c.reset()

	//处理web请求
	engine.handleHTTPRequest(c)

	//将Context对象扔回对象池了
	engine.pool.Put(c)
}

handleHTTPRequest

下面的代码省略了很多和核心逻辑无关的代码,核心逻辑很简单:更具请求方法和请求的URI找到处理函数们,然后调用。为什么是处理函数们,而不是我们写的处理函数?因为这里包括了中间层的处理函数。

func (engine *Engine) handleHTTPRequest(context *Context) {
	httpMethod := context.Request.Method
	var path string
	var unescape bool

	// 省略......
	// tree是个数组,里面保存着对应的请求方式的,URI与处理函数的树。
	// 之所以用数组是因为,在个数少的时候,数组查询比字典要快
	t := engine.trees
	for i, tl := 0, len(t); i < tl; i++ {
		
		if t[i].method == httpMethod {
			root := t[i].root
			
			// 找到路由对应的处理函数们
			handlers, params, tsr := root.getValue(path, context.Params, unescape)
			
			// 调用处理函数们
			if handlers != nil {
				context.handlers = handlers
				context.Params = params
				context.Next()
				context.writermem.WriteHeaderNow()
				return
			}
			
			// 省略......
			break
		}
	}

	// 省略......
}

值得欣赏学习的地方

路由处理

关键需求

先抛开gin框架不说,路由处理的关键需求有哪些?个人认为有以下两点

  • 高效的URI对应的处理函数的查找
  • 灵活的路由组合

gin的处理

核心思路

  • 每一个路由对应的都有一个独立的处理函数数组
  • 中间件与处理函数是一致的
  • 利用树提供高效的URI对应的处理函数数组的查找

有趣的地方

RouterGroup对路由的处理

灵活的路由组合是通过将每一个URI都应用着一个独立的处理函数数组来实现的。对于路由组合的操作抽象出了RouterGroup结构体来应对。它的主要作用是:

  • 将路由与相关的处理函数关联起来
  • 提供了路由组的功能,这个是由于关联前缀的方式实现的
  • 提供了中间件自由组合的功能:1. 总的中间件 2. 路由组的中间件 3.处理函数的中间件

路由组和处理函数都可以添加中间件这比DJango那种只有总的中间件要灵活得多。

中间件的处理

中间件在请求的时候需要处理,在返回时也可能需要做处理。如下图(图是django的)。

中间件的处理
问题来了在gin中间件就是一个处理函数,怎么实现返回时的处理呢。仔细观察,上面图的调用,就是后进先出,是的每错答案就是:利用函数调用栈后进先出的特点,巧妙的完成中间件在自定义处理函数完成的后处理的操作。django它的处理方式是定义个类,请求处理前的处理的定义一个方法,请求处理后的处理定义一个方法。gin的方式更灵活,但django的方式更加清晰。

//调用处理函数数组
func (c *Context) Next() {
	c.index++
	s := int8(len(c.handlers))
	for ; c.index < s; c.index++ {
		c.handlers[c.index](c)
	}
}

// 中间件例子
func Logger() gin.HandlerFunc {
	return func(c *gin.Context) {
		t := time.Now()

		// before request
		c.Set("example", "12345")

		c.Next()

		// 返回后的处理
		latency := time.Since(t)
		log.Print("latency: ", latency)

		status := c.Writer.Status()
		log.Println("status: ", status)
	}
}

func main() {
	r := gin.New()
	r.Use(Logger())

	r.GET("/test", func(c *gin.Context) {
		example := c.MustGet("example").(string)

		// it would print: "12345"
		log.Println("example", example)
	})

	// Listen and serve on 0.0.0.0:8080
	r.Run(":8081")
}

请求内容的处理与返回内容的处理

需求

  • 获取路径当中的参数
  • 获取请求参数
  • 获取请求内容
  • 将处理好的结果返回

Gin框架的实现思路

自己包装一层除了能提供体验一致的处理方法之外,如果对官方实现的不爽,可以替换掉,甚至可以加一层缓存处理(其实没必要,因为正常的使用,仅仅只会处理一次就够了)。

  • 如果官方的http库能提供的,则在官方的http库只上包装一层,提供体验一致的接口。
  • 官方http库不能提供的,则自己实现

关键结构体

type Context struct {
	writermem responseWriter
	Request   *http.Request

	// 传递接口,使用各个处理函数,更加灵活,降低耦合
	Writer    ResponseWriter
	
	Params   Params				// 路径当中的参数
	handlers HandlersChain		// 处理函数数组
	index    int8				// 目前在运行着第几个处理函数

	engine   *Engine
	Keys     map[string]interface{}  // 各个中间件添加的key value
	Errors   errorMsgs
	Accepted []string
}

值得学习的点

在数量少的情况下用数组查找值,比用字典查找值要快

在上面对Context结构体的注释当中,可以知道Params其实是个数组。本质上可以说是key值的对应,为啥不用字典呢,而是用数组呢? 实际的场景,获取路径参数的参数个数不会很多,如果用字典性能反而不如数组高。因为字典要找到对应的值,大体的流程:对key进行hash —> 通过某算法找到对应偏移的位置(有好几种算法,有兴趣的可以去查查看) —> 取值。一套流程下来,数组在量少的情况下,已经遍历完了。

router.GET("user/:name/*action", func(c *gin.Context) {
		name := c.Param("name")
		action := c.Param("action")
		message := name + " is222 " + action
		c.String(http.StatusOK, message)
	})
   
func (ps Params) Get(name string) (string, bool) {
	for _, entry := range ps {
		if entry.Key == name {
			return entry.Value, true
		}
	}
	return "", false
}

通过接口处理有所同,有所不同的场景

获取请求内容

对于获取请求内容这个需求面临着的场景。对于go这种静态语言来说,如果要对请求内容进行处理,就需要对内容进行反序列化到某个结构体当中,然而请求内容的形式多种多样,例如:JSON,XML,ProtoBuf等等。因此这里可以总结出下面的非功能性需求。

  • 不同的内容需要不同的反序列化机制
  • 允许用户自己实现反序列化机制

共同点都是对内容做处理,不同点是对内容的处理方式不一样,很容易让人想到多态这概念,异种求同。多态的核心就是接口,这时候需要抽象出一个接口。

type Binding interface {
	Name() string
	Bind(*http.Request, interface{}) error
}

将处理好的内容返回

请求内容多种多样,返回的内容也是一样的。例如:返回JSON,返回XML,返回HTML,返回302等等。这里可以总结出以下非功能性需求。

  • 不同类型的返回内容需要不同的序列化机制
  • 允许用户实现自己的序列化机制

和上面的一致的,因此这里也抽象出一个接口。

type Render interface {
	Render(http.ResponseWriter) error
	WriteContentType(w http.ResponseWriter)
}

接口定义好之后需要思考如何使用接口

思考如何优雅的使用这些接口

对于获取请求内容,在模型绑定当中,有以下的场景

  • 绑定失败是用户自己处理还是框架统一进行处理
  • 用户需是否需要关心请求的内容选择不同的绑定器

在gin框架的对于这些场景给出的答案是:提供不同的方法,满足以上的需求。这里的关键点还是在于使用场景是怎样的。

// 自动更加请求头选择不同的绑定器对象进行处理
func (c *Context) Bind(obj interface{}) error {
	b := binding.Default(c.Request.Method, c.ContentType())
	return c.MustBindWith(obj, b)
}

// 绑定失败后,框架会进行统一的处理
func (c *Context) MustBindWith(obj interface{}, b binding.Binding) (err error) {
	if err = c.ShouldBindWith(obj, b); err != nil {
		c.AbortWithError(400, err).SetType(ErrorTypeBind)
	}

	return
}

// 用户可以自行选择绑定器,自行对出错处理。自行选择绑定器,这也意味着用户可以自己实现绑定器。
// 例如:嫌弃默认的json处理是用官方的json处理包,嫌弃它慢,可以自己实现Binding接口
func (c *Context) ShouldBindWith(obj interface{}, b binding.Binding) error {
	return b.Bind(c.Request, obj)
}

对于实现的结构体构造不一致的处理

将处理好的内容返回,实现的类构造参数都是不一致的。例如:对于文本的处理和对于json的处理。面对这种场景祭出的武器是:封装多一层,用于构造出相对于的处理对象。

//对于String的处理
type String struct {
	Format string
	Data   []interface{}
}

//对于String处理封装多的一层 
func (c *Context) String(code int, format string, values ...interface{}) {
	c.Render(code, render.String{Format: format, Data: values})
}

//对于json的处理
JSON struct {
	Data interface{}
}

//对于json的处理封装多的一层
func (c *Context) JSON(code int, obj interface{}) {
	c.Render(code, render.JSON{Data: obj})
}

//核心的一致的处理
func (c *Context) Render(code int, r render.Render) {
	c.Status(code)

	if !bodyAllowedForStatus(code) {
		r.WriteContentType(c.Writer)
		c.Writer.WriteHeaderNow()
		return
	}

	if err := r.Render(c.Writer); err != nil {
		panic(err)
	}
}

总结

这个看代码的过程是在有目标之后,按照官方文档的例子,一步一步的看的。然后再慢慢欣赏,这框架对于一些web框架常见的场景,它是怎么处理。这框架的代码量很少,而且写得十分的优雅,非常值得一看。

猜你喜欢

转载自juejin.im/post/5b38a6b16fb9a00e6714ab3a
今日推荐