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写入,方法很全,实际用的时候根据自己项目实际情况而定。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
2020-12-19 系统服务配置 服务(Service)-详细版
2020-12-19 systemd.service配置