Gin中的gin.Context与Golang原生的context.Context区别与联系

一.gin中的context gin.Context

1.概念

在 Gin 中,Context 是一个非常重要的概念,它是Gin的核心结构体之一,用于处理 HTTP 请求和响应,在 Gin 的处理流程中,Context 贯穿整个处理过程,用于传递请求响应的信息Gin 的 Context 是一个结构体类型,核心定义如下:

Go 复制代码
type Context struct {
    // 定义了一些私有成员变量,用于存储请求和响应等信息
    ...
    writermem responseWriter
    Request   *http.Request  // 保存request请求
    Writer    ResponseWriter // 回写response 
    Params   Params
    handlers  HandlersChain  // 该次请求所有的中间件函数和处理函数
    index     int8           // HandlersChain的下标,用来调用某个具体的HandlerFunc
    fullPath  string         // 请求的url路径
    engine    *Engine        // 对server Engine的引用
    Keys      map[string]any // 用于上下游之间传递参数
    params       *Params
	skippedNodes *[]skippedNode
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
    ...
    // ...
}

2.中间件

Context 中封装了原生的 Go HTTP 请求响应对象 ,同时还提供了一些处理请求和生成响应的各种方法,用于获取请求和响应的信息设置响应 头、设置响应状态 码等操作.在Gin中,Context 是通过中间件来传递的,在处理 HTTP 请求时,Gin 会依次执行注册的中间件,每个中间件可以对 Context 进行一些操作,然后将 Context 传递给下一个中间件。

例如,下面是一个简单的中间件,用于在请求头中设置一个自定义的 X-Request-ID:

Go 复制代码
func RequestIDMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        requestID := generateRequestID()
        c.Request.Header.Set("X-Request-ID", requestID)
        c.Next()  // 将 Context 传递给下一个中间件
    }
}

在上面的中间件中,生成了一个请求 ID,然后将其设置到请求头中,接着,调用 c.Next() 方法将Context 传递给下一个中间件,这样,下一个中间件就可以通过**c.Request.Header.Get("X-Request-ID")获取到这个请求 ID
从上面可以知道:一次请求的所有中间件函数和请求处理函数都在
Context.handlers中,因此,当请求到来时,只需要依次调用Context.handlers中的所有HandlerFunc** 即可,这就是调用中间件中的一个最重要的函数**Next(),**定义如下:

Go 复制代码
func (c *Context) Next() {
    c.index++  // 依次遍历所有的中间件函数,并调用他们
    for c.index < int8(len(c.handlers)) {
        c.handlers[c.index](c)   
        c.index++
    }
}

除了顺序执行所有中间件,还要有在某个中间件函数中终止处理的能力,比如某个中间件负责权限校验,如果用户的校验没通过,直接返回Not Authorized,跳过后续的处理,这个是通过Abort函数实现的:

Go 复制代码
func (c *Context) Abort() {
    c.index = abortIndex // abortIndex是个常量=63
}

abort()的原理非常简单:

直接让c.Index等于最大值,这样剩余的中间件函数都没机会执行,从这个函数中可以看出,中间件的数量是有上限的 ,上限就是63个
正常的执行流是依次调用各个中间件函数,但是如果在某个中间件函数中显式调用了Next(),会先执行后续的中间件,执行完成了再返回当前中间件继续执行,流程如下:

3.参数传递

Context中有成员Keys ,上游需要传递的变量可以放在里面,下游处理时再从里面取出来,实现上下游参数传递,对应Set()Get()方法

Go 复制代码
func (c *Context) Set(key string, value any) {
    c.mu.Lock() // 用来保护c.Keys并发安全
    if c.Keys == nil {
        c.Keys = make(map[string]any)
    }

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

func (c *Context) Get(key string) (value any, exists bool) {
    c.mu.RLock()
    value, exists = c.Keys[key]
    c.mu.RUnlock()
    return
}

Get()函数还有很多衍生函数,比如GetString,GetStringMapString,GetStringMapStringSlice等,都是在Get的基础上,将数据转换成我们需要的类型再返回

4.参数绑定

请求放到Context.Request里,需要解析成结构体或map,才好在业务代码里使用,这一部分是通过Bind()系列函数实现的,Bind()系列函数的作用就是根据request的数据类型,将其解析到结构体或map里,简单的看一个参数绑定的实现:

Go 复制代码
func (c *Context) ShouldBindWith(obj any, b binding.Binding) error {
    return b.Bind(c.Request, obj)  // 底层调用的是binding相关的函数
}

针对不同的请求,gin提供了很多函数,用于解析对应的参数,常用的函数如下:

Go 复制代码
Param(key string) string  // 用于获取url参数,比如/welcome/:user_id中的user_id

// 获取GET请求中携带的参数
GetQueryArray(key string) ([]string, bool)  
GetQuery(key string)(string, bool)
Query(key string) string
DefaultQuery(key, defaultValue string) string

// 获取POST请求参数
GetPostFormArray(key string) ([]string, bool)
PostFormArray(key string) []string 
GetPostForm(key string) (string, bool)
PostForm(key string) string
DefaultPostForm(key, defaultValue string) string

// data binding 
Bind (obj interface {}) error // bind data according to Content-Type
BindJSON(obj interface{}) error
BindQuery(obj interface{}) error

ShouldBind(obj interface{}) error
ShouldBindJSON(obj interface{}) error
ShouldBindQuery(obj interface{}) error
...

其中Bind相关的函数是一种比较通用的方法,它允许我们将请求参数填充到一个map或struct中,这样在后续请求处理时,能够方便的使用传入的参数。Bind相关的函数分为三大类:

Go 复制代码
// 1. Bind函数
Bind (obj interface {}) error // 根据请求中content-type的类型来选择对应的具体Bind函数,底层调用的是BindJson, BindQuery这种具体的Bind函数

// 2. BindXXX(),具体的Bind函数,用于绑定一种参数类型,底层调用MustBindWith或者ShouldBindWith
BindJSON(obj interface{}) error
BindQuery(obj interface{}) error

// 3. 最底层的基础函数
MustBindWith(obj any, b binding.Binding) error // 当出现参数校验问题时,会直接返回400,底层仍然是ShouldBindWith
ShouldBindWith(obj any, b binding.Binding) error

其中各种类型的Bind函数,最底层的调用都是ShouldBindWith()函数,该函数有两个参数,第一个参数obj为需要将参数填充进去的对象(本文称为填充对象);第二个参数为binding.Binding类型,该类型的定义在package binding中,如下:

Go 复制代码
type Binding interface {
    Name() string
    Bind(*http.Request, any) error
}

Bingding为一个接口,提供了Name()和Bind()两个函数,Name()负责返回对应的Bind类型,比如JSON,XML等,Bind()函数则负责实现具体类型的参数绑定。为了完成常用参数类型的绑定,gin给每种参数类型都定义了一个类,并实现Binding接口,具体实现了Binding接口的类如下:

Go 复制代码
// 实现Binding接口的具体类
var (
    JSON          = jsonBinding{}
    XML           = xmlBinding{}
    Form          = formBinding{}
    Query         = queryBinding{}
    FormPost      = formPostBinding{}
    FormMultipart = formMultipartBinding{}
    ProtoBuf      = protobufBinding{}
    MsgPack       = msgpackBinding{}
    YAML          = yamlBinding{}
    Uri           = uriBinding{}
    Header        = headerBinding{}
    TOML          = tomlBinding{}
)

这样,每当一个HTTP请求到来的时候,用户可以直接调用Bind()函数,Bind()函数可以通过content-type选择对应的实现了Bind()方法的对应类的实例,调用其Bind()方法,完成参数绑定.

常用的HTTP请求参数类型HTTP GET query参数类型HTTP POST json参数类型,以这两个为例,看一下参数绑定的一些细节:

BindJSON:

jsonBinding对Binding的实现如下:

json的参数绑定比较简单,使用json.Decoder完成

Go 复制代码
func (jsonBinding) Name() string {
    return "json"
}

func (jsonBinding) Bind(req *http.Request, obj any) error {
    if req == nil || req.Body == nil {
        return errors.New("invalid request")
    }
    return decodeJSON(req.Body, obj)
}

func decodeJSON(r io.Reader, obj any) error {
    decoder := json.NewDecoder(r) // 使用json.Decoder对json进行解析
    if EnableDecoderUseNumber {
        decoder.UseNumber()
    }
    if EnableDecoderDisallowUnknownFields {
        decoder.DisallowUnknownFields()
    }
    if err := decoder.Decode(obj); err != nil {
        return err
    }
    return validate(obj) // 参数校验
}

BindQuery:

一个HTTP GET请求的query参数,在go中,可以通过request.URL.Query()获取到,获取到的query参数类型为url.Values,因此,BindQuery()首先获取到url.Values类型的query参数,然后设置对应的值,BindQuery()根据传进来的要将参数填充进去的对象类型(本文称为填充对象,是map类型还是struct ptr),分成了两个填充函数:

Go 复制代码
/* mapFormByTag是queryBinding.Bind()的底层核心函数
* ptr: 填充对象,可能是map或strcut的指针
* form: url.Values,包含所有query参数
* tag: 值为"form"
*/
func mapFormByTag(ptr any, form map[string][]string, tag string) error {
    // Check if ptr is a map
    ptrVal := reflect.ValueOf(ptr)
    var pointed any
    if ptrVal.Kind() == reflect.Ptr {
        ptrVal = ptrVal.Elem()
        pointed = ptrVal.Interface()
    }
    if ptrVal.Kind() == reflect.Map &&
        ptrVal.Type().Key().Kind() == reflect.String {
        if pointed != nil {
            ptr = pointed
        }
        return setFormMap(ptr, form) // 如果填充对象是map类型
    }

    return mappingByPtr(ptr, formSource(form), tag) // 填充对象是ptr struct类型
}

接下来分别看一下两个核心的填充函数setFormMap()和mappingByPtr()

Go 复制代码
// setFormMap本身比较简单,因为query参数(url.Values是map[string][]string类型的别称)本身就是map[string][]string类型,只需要判断填充对象是map[string]string还是map[string][]string类型
func setFormMap(ptr any, form map[string][]string) error {
    el := reflect.TypeOf(ptr).Elem() // 因为ptr本身是map类型,Elem返回该map的value值
    // 如果map填充对象的value值为[]string类型,直接填充
    if el.Kind() == reflect.Slice {
        ptrMap, ok := ptr.(map[string][]string)
        if !ok {
            return ErrConvertMapStringSlice
        }
        for k, v := range form {
            ptrMap[k] = v
        }

        return nil
    }
    // 否则,map填充对象为map[string]string类型,取url.Values每个key对应的value值(类型为[]string)的最后一个元素填充到填充对象中
    ptrMap, ok := ptr.(map[string]string)
    if !ok {
        return ErrConvertToMapString
    }
    for k, v := range form {
        ptrMap[k] = v[len(v)-1] // pick last
    }

    return nil
}

mappingByPtr()的逻辑比较复杂,核心是遍历struct的每一个字段,获取该struct字段的json tag,如果tag的值跟某一个url.Values的key的值相等,填充对应的值

Go 复制代码
func mappingByPtr(ptr any, setter setter, tag string) error {
    _, err := mapping(reflect.ValueOf(ptr), emptyField, setter, tag)
    return err
}

/* mapping是一个递归函数,因为struct填充对象有可能嵌套了ptr成员
* value:填充对象
* field:struct填充对象的某个具体成员变量
* setter:内部包含了url.Values
* tag: 等于"form"
*/
func mapping(value reflect.Value, field reflect.StructField, setter setter, tag string) (bool, error) {
    if field.Tag.Get(tag) == "-" { // just ignoring this field
        return false, nil
    }

    vKind := value.Kind()

    // 如果填充对象是ptr,获取其指向的对象递归调用mapping
    if vKind == reflect.Ptr {
        var isNew bool
        vPtr := value
        if value.IsNil() {
            isNew = true
            vPtr = reflect.New(value.Type().Elem())
        }
        isSet, err := mapping(vPtr.Elem(), field, setter, tag) 
        if err != nil {
            return false, err
        }
        if isNew && isSet {
            value.Set(vPtr)
        }
        return isSet, nil
    }
    // 递归到最底层,每个value都是StructField类型,开始填充值
    if vKind != reflect.Struct || !field.Anonymous {
        ok, err := tryToSetValue(value, field, setter, tag)
        if err != nil {
            return false, err
        }
        if ok {
            return true, nil
        }
    }
    // 如果填充对象是struct,针对每一个struct field,递归调用mapping
    if vKind == reflect.Struct {
        tValue := value.Type()

        var isSet bool
        for i := 0; i < value.NumField(); i++ {
            sf := tValue.Field(i)
            if sf.PkgPath != "" && !sf.Anonymous { // unexported
                continue
            }
            ok, err := mapping(value.Field(i), sf, setter, tag)
            if err != nil {
                return false, err
            }
            isSet = isSet || ok
        }
        return isSet, nil
    }
    return false, nil
}

总结: Gin通过区分不同的参数类型,每种参数类型实现了统一的**Bind()**函数来完成对应的参数绑定,用户只需要调用统一的函数,不用关系底层实现细节,即可完成参数绑定
从上面介绍可以知道:

在 Gin 中,Context 是通过中间件来传递的,每个中间件都可以对 Context 进行一些操作,然后将其传递给下一个中间件,最终由处理函数来使用Context中的信息进行处理

二.gin.Context和context.Context的区别

Gin 的 gin.Context 和 Go标准库中的 context.Context 是两个不同的概念,用途也不同

  • gin.Context:是 Gin 框架中的一个结构体类型,用于封装 HTTP 请求和响应的信息,以及提供一些方法,用于获取请求和响应的信息、设置响应头、设置响应状态码等操作,gin.Context 只在 Gin 框架内部使用,用于处理 HTTP 请求和响应,它与 HTTP 请求和响应一一对应,每个 HTTP 请求都会创建一个新的 gin.Context 对象,并在处理过程中传递
  • context.Context:是 Go 标准库中的一个接口类型,用于在Goroutine 之间传递上下文信息,context.Context 可以在 Goroutine 之间传递信息,例如传递请求 ID、数据库连接、请求超时等信息,context.Context 的具体实现是由各种库框架提供的,例如Gin 框架中也提供了一个 gin.Context 的实现,用于在 Gin 框架中使用 context.Context
    总之,gin.Context是Gin框架中用于处理HTTP请求和响应的上下文对象,而context.Context是Go标准库中用于在Goroutine之间传递上下文信息的接口类型,在使用 Gin 框架时,可以通过 gin.Context的Request.Context()方法来访问 context.Context对象,从而在 Gin 框架中使用上下文信息
    例如,可以在中间件中设置一个请求 ID,并将其保存在 context.Context 中,然后在处理函数中获取这个请求 ID,下面是一个示例:
Go 复制代码
func RequestIDMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        requestID := generateRequestID()
        ctx := context.WithValue(c.Request.Context(), "requestID", requestID)
        c.Request = c.Request.WithContext(ctx)
        c.Next()
    }
}

func HelloHandler(c *gin.Context) {
    requestID := c.Request.Context().Value("requestID").(string)
    c.String(http.StatusOK, "Hello, request ID = %s", requestID)
}

在上面的示例中,中间件 RequestIDMiddleware会生成一个请求ID,并将其保存在context.Context中,然后,将这个 context.Context 对象保存到 http.Request 中,在处理函数 HelloHandler 中,可以通过 c.Request.Context().Value("requestID") 来获取这个请求 ID
**总之,**在 Gin 中,可以通过 gin.Context 来访问 context.Context,从而在 Gin 框架中传递上下文信息,开发者可以在中间件中设置 context.Context,然后在处理函数中获取这个上下文信息

相关推荐
落落落sss7 分钟前
MybatisPlus
android·java·开发语言·spring·tomcat·rabbitmq·mybatis
简单.is.good25 分钟前
【测试】接口测试与接口自动化
开发语言·python
Yvemil744 分钟前
MQ 架构设计原理与消息中间件详解(二)
开发语言·后端·ruby
程序员是干活的1 小时前
私家车开车回家过节会发生什么事情
java·开发语言·软件构建·1024程序员节
我是陈泽1 小时前
一行 Python 代码能实现什么丧心病狂的功能?圣诞树源代码
开发语言·python·程序员·编程·python教程·python学习·python教学
优雅的小武先生1 小时前
QT中的按钮控件和comboBox控件和spinBox控件无法点击的bug
开发语言·qt·bug
虽千万人 吾往矣1 小时前
golang gorm
开发语言·数据库·后端·tcp/ip·golang
创作小达人1 小时前
家政服务|基于springBoot的家政服务平台设计与实现(附项目源码+论文+数据库)
开发语言·python
郭二哈1 小时前
C++——list
开发语言·c++·list
杨荧1 小时前
【JAVA开源】基于Vue和SpringBoot的洗衣店订单管理系统
java·开发语言·vue.js·spring boot·spring cloud·开源