gin源码学习-context(4)


gin的context封装了request和response,gin框架在处理具体的请求时,也都是以context作为载体,gin的context的覆盖了很多功能,在gin的源码中其实已经很简单明了了,接下来讲分享个人对gin.context的一些理解。

首先看看gin中对context结构体的定义:

// Context is the most important part of gin. It allows us to pass variables between middleware,
// manage the flow, validate the JSON of a request and render a JSON response for example.
type Context struct {
	writermem responseWriter
	// 封装请求
	Request   *http.Request
	// 封装响应
	Writer    ResponseWriter

	Params   Params
	handlers HandlersChain
	index    int8
	fullPath string

	engine       *Engine
	params       *Params
	skippedNodes *[]skippedNode

	// This mutex protects Keys map.
	mu sync.RWMutex

	// Keys is a key/value pair exclusively for the context of each request.
	Keys map[string]any

	// Errors is a list of errors attached to all the handlers/middlewares who used this context.
	Errors errorMsgs

	// Accepted defines a list of manually accepted formats for content negotiation.
	Accepted []string

	// queryCache caches the query result from c.Request.URL.Query().
	queryCache url.Values

	// formCache caches c.Request.PostForm, which contains the parsed form data from POST, PATCH,
	// or PUT body parameters.
	formCache url.Values

	// SameSite allows a server to define a cookie attribute making it impossible for
	// the browser to send this cookie along with cross-site requests.
	sameSite http.SameSite
}

1.context creation

顾名思义,就是与context的创建复制相关的方法。

Context.reset()

此方法用来重置context,具体来说就是将Writer/cache/Keys/handlers全部重置,具体主要用在2各方面,一是test用,二是处理具体请求(如Engine.ServeHTTP(),每次处理新请求,pool中取context,重置,使用,再放回)。

Context.Copy()

新建一个context,原有context内的数据复制一份到新context中。在gin中,如果需要重开goroutine时,官方建议使用此方法,借此传递Context。

Context.HandlerName()

用以返回当前context的handler名,是handlers的最后一个,因为按照gin的路由机制,每个路由下对应的handlers其实就是一个handler的切片,其中最后一个是真正请求处理器,其余都看作中间件。

Context.HandlerNames()/Handler()

前者返回当前的handlers的切片,后者返回处理器函数。

总体来说,这块的功能相对简单,也很好理解,大家稍微看看即懂。

2.flow control-流控制

Context.Next()

该方法仅用在中间件中,在正常的业务逻辑中不用,可以看看源码:

func (c *Context) Next() {
	c.index++
	for c.index < int8(len(c.handlers)) {
		c.handlers[c.index](c)
		c.index++
	}
}

我们知道c.handlers实际是HandlerChain切片,就是每个路由对应的处理器组,所以就是遍历所有的handlers函数,挨个处理。

Context.Abort()

此方法相当于阻断当前数据流,不再向下传递请求,比如我们可以在认证的中间件中使用,如果需要认证的请求但未认证,我们可以在此使用,不再传递,提前返回,或者是在业务逻辑中,dao层处理返回某些错误,提前返回。

Context.AbortWithStatus()/AbortWithStatusJSON()/AbortWithError()

此类方法即相当于Abort()时,加入code或者是正确错误返回,c.Writer中加入相关响应。

3.error management

直接上段源码看看:

func (c *Context) Error(err error) *Error {
	if err == nil {
		panic("err is nil")
	}

	var parsedError *Error
	ok := errors.As(err, &parsedError)
	if !ok {
		parsedError = &Error{
			Err:  err,
			Type: ErrorTypePrivate,
		}
	}

	c.Errors = append(c.Errors, parsedError)
	return parsedError
}

gin本身有这个初衷很好,看注释官方是想着,这样的应用场景,用一个中间件收集error,遇到error就放到c.Errors切片中,最后可以通过打印log或者记录到数据库中,实现error的记录。

不过我在自己的项目中并没有用到这个方法。

4.metadata management

从context结构体的定义可以看到,Context.Keys其实定义了一个map结构,很多时候我们可以直接用到这个属性,比如控制器函数的前置后置处理,这个使用gin为我们提供了相应的Get()/Set()方法。

Context.Set()

// Set is used to store a new key/value pair exclusively for this context.
// It also lazy initializes  c.Keys if it was not used previously.
func (c *Context) Set(key string, value any) {
	c.mu.Lock()
	if c.Keys == nil {
		c.Keys = make(map[string]any)
	}

	c.Keys[key] = value
	c.mu.Unlock()
}

考虑到并发,gin为我们实现了并发安全地设置map,也不限type,只要key是string即可。

Context.Get()

// Get returns the value for the given key, ie: (value, true).
// If the value does not exist it returns (nil, false)
func (c *Context) Get(key string) (value any, exists bool) {
   c.mu.RLock()
   value, exists = c.Keys[key]
   c.mu.RUnlock()
   return
}

获取context中设置的内容也很简单,该方法返回两个值,一个是具体的value,一个是是否存在的bool值,具体的应用就看业务逻辑怎样用了。

除了Get()这个方法外,gin还贴心地提供了n多Getxxx()方法,比如不存在就panic的MustGet(),根据value的type的GetString/GetInt/GetBool/GetTime/GetStringSlice等,基本都用到Get()方法,然后在做个类型断言。

举个例子,比如我们调用了c.Set("key-xxx", []string{""}),然后调用c.GetStringSlice("key-xxx"):

// GetStringSlice returns the value associated with the key as a slice of strings.
func (c *Context) GetStringSlice(key string) (ss []string) {
	if val, ok := c.Get(key); ok && val != nil {
		ss, _ = val.([]string)
	}
	return
}

5.input data

gin框架设计的api是Restful风格的,我们拿到请求的传入参数不外乎这样几种途径:

  • query参数
  • form表单
  • 路由的可变参数,或者是动态路由
  • json/yaml/toml/xml格式的request body

动态路由参数

比如我们注册了这样一个路由:router.Get("/app/v1/user/:userId", xxx),其中userId这个参数就是一个动态的url参数,我们此时就可以使用c.Param("userId")获取对应参数,默认""。

query参数

看看源码的具体实现:

// Query returns the keyed url query value if it exists,
// otherwise it returns an empty string `("")`.
// It is shortcut for `c.Request.URL.Query().Get(key)`
//     GET /path?id=1234&name=Manu&value=
// 	   c.Query("id") == "1234"
// 	   c.Query("name") == "Manu"
// 	   c.Query("value") == ""
// 	   c.Query("wtf") == ""
func (c *Context) Query(key string) (value string) {
	value, _ = c.GetQuery(key)
	return
}

可以看到,直接调用c.Query("key-name")即可,是不是很简便呢?

当然gin也提供了带有默认值的DefaultQuery(key, defaultValue string) string以及带有判断的GetQuery(key string) (string, bool),如果query参数的array或者map,也都不在话下,因为gin提供了QueryArray、QueryMap等。

form表单

gin在表单参数方面也提供了类似query参数提取的方法:

  • PostForm(key string) (value string),根据可以提取val,限定string类型,默认没有即返回“”
  • DefaultPostForm(key, defaultValue string) string,没有即返回default值
  • GetPostForm(key string) (string, bool),带判断的返回
  • PostFormArray(key string) (values []string)/PostFormMap,解析array/map类型的form参数

或者当涉及到文件上传时,gin提供了FormFile(name string) (*multipart.FileHeader, error),gin的context甚至还提供了文件上传的实现-SaveUploadedFile(file *multipart.FileHeader, dst string) error,比如我们通过FormFile()拿到fh,然后调用SaveUploadedFile()即可实现文件的上传。

json/yaml/toml/xml格式的request body

我们日常的api开发中最常用的就是通过json格式的数据作为body的传递格式,这里以json格式的body为例说明。

通常在请求体中的首部header中,Content-Type:application/json,这是较为常见的。问题来了,我们在具体的业务逻辑中又怎么获取到这些body呢。

gin的context为我们提供一系列的BindXXX方法:

  • Bind()/BindJSON()/BindXML()/BindYAML()
  • MustBindWith(obj any, b binding.Binding) error
  • ShouldBind()/ShouldBindJSON()...
  • ShouldBindWith()/ShouldBindBodyWith()

这是最基础的两个Bindxxx方法,根据官方的注解,如果追求性能,就用ShouldBindWith(),如果多次解析body,考虑重用,就用ShouldBindBodyWith()。其中ShouldBind()实际调用ShouldBindWith(),ShouldBindJSON()只是指明binding.type,MustBindWith()的解析则带有异常解析提前返回的处理,实际也是调用ShouldBindWith(),此外所有的Bindxxx()都是MustBindWith()加对应type的解析。

建议:如果项目中的context涉及重用context,就用ShouldBindBodyWith(),否则用ShouldBindWith()。

其他

  • ClientIP() string,解析客户端IP
  • RemoteIP() string,通过Request.RemoteAddr解析

6.response rendring

这里涉及的功能都是为了写入响应,比如写入StatusCode,Header,或者是通过HTML/String/Json等格式写到响应体中。

Header/GetHeader

Header()是写入到响应首部,对应签名:Header(key, value string) {}。

GetHeader()则是从请求中获取请求首部的信息,对应签名:GetHeader(key string) string {}。

Cookie/SetCookie

Cookie()是为了获取cookie,SetCookie则是设置cookie,具体这里详细叙述,有兴趣可自行搜索。

JSON/XML/HTML/String

这类的方法都是Render功能,只是序列化格式差异,比如我们在api中可以直接:

ctx.JSON(200, gin.H{})
ctx.String(200, gin.H{})

使用也比较简单,就是不同序列化格式而已,具体使用需要和前端同事协定好格式。

7.context.Context()的实现

gin.Context也实现了标准库的context.Context()接口,但一般没见很多应用,我的项目中也没有用到。

总结:

gin.ConText主要为我们实现了request/response的封装,最典型的就是请求的解析,响应的写入,前置后置处理的map写入,方法很全,实际用的时候根据自己项目实际情况而定。

posted on 2022-12-19 15:39  进击的davis  阅读(895)  评论(0编辑  收藏  举报

导航