[Go package] Gin 的 context 解析

2,695 阅读8分钟

Context 是 gin 中最重要的部分。

一、数据结构

type Context struct {
   writermem responseWriter              //相应处理
   Request   *http.Request               //请求报文的抽象
   Writer    ResponseWriter              //响应报文的抽象

   Params   Params                       //记录了url参数,是一组键值对
   handlers HandlersChain                //定义一个 HandlerFunc 数组
   index    int8                         //定义一个HandlerFunc数组的索引。next() / abort() 会用到
   fullPath string                      //请求地址

   engine       *Engine                 //gin 框架实例对象,gin.New()返回的就是一个 *Engine 实例
 
   params       *Params                 //URL 参数的切片
   skippedNodes *[]skippedNode

   mu sync.RWMutex                     //读写锁,用来保护Keys

   
   Keys map[string]any                 //是专门针对每个请求的上下文的键/值对

   

   Errors errorMsgs                   //使用此上下文的所有处理程序或者中间件附带的错误列表

   
   Accepted []string                  //自定义请求接收的内容类型格式

   
   queryCache url.Values              //底层数据类型是 map[string][]string 。使用url.ParseQuery()从c.Request.URL.Query()缓存了参数查询结果


   formCache url.Values              //使用url.ParseQuery缓存的PostForm包含来自POST,PATCH或者PUT请求参数

   sameSite http.SameSite           //允许服务器定义cookie属性。用来防止 CSRF 攻击和用户追踪
}

二、元数据管理

这类的数据管理专门用于为此上下文存储新的键值对。存储在Context中的Keys数据字段中,如果以前没有使用过,它会延迟初始化。

提供两个基本函数Get()、Set()完成对Keys的读写操作,都有读写锁以实现并发安全。

1. Set()

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

2. Get()

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

3. 基于Get()的不同特定数据类型函数

// MustGet(key string) interface{} 返回给定键的值(如果存在),否则会panic。
// GetString(key string) (s string)
// GetBool(key string) (b bool)
// GetInt(key string) (i int)
// GetInt64(key string) (i64 int64)
// GetUint(key string) (ui uint)
// GetUint64(key string) (ui64 uint64)
// GetFloat64(key string) (f64 float64)
// GetTime(key string) (t time.Time)
// GetDuration(key string) (d time.Duration)
// GetStringSlice(key string) (ss []string)
// GetStringMap(key string) (sm map[string]interface{})
// GetStringMapString(key string) (sms map[string]string)
// GetStringMapStringSlice(key string) (smss map[string][]string)

//举例
func (c *Context) GetInt64(key string) (i64 int64) {
   if val, ok := c.Get(key); ok && val != nil {
      i64, _ = val.(int64)       //通过添加类型断言来完成指定想要的值
   }
   return
}

三、获取请求数据

1. Param 数据

  • gin 源码

func (c *Context) Param(key string) string {
   return c.Params.ByName(key)
}

当路由是
router.GET("/user/:id", func(c *gin.Context) {})
这种形式时,可以通过 id := c.Param("id")从 Context 对象的Params字段来获取到id的参数值。Params保存了若干 key-value 对。其中

  • key 是一条 route 的 url 所设定的:后的单词,本例中就是id
  • value 是实际传来的请求 url 的实际数据。下例为 5

GET localhost:3000/user/5

  • 实例

  • 实例代码
	engine.GET("/test/:name/:age/:height", func(c *gin.Context) {
   	// 接收参数
   	`name := c.Param("name")`
   	`age := c.Param("age")`
   	`height := c.Param("height")`
               
   	c.JSON(200, gin.H{
   		"msg": "success",
   		"name": name,
   		"phone":age,
   		"height":height,
   	})
   })
  • 响应
➜ curl -X GET http://127.0.0.1:8080/test/张三/18/170
{"height":"170","msg":"success","name":"张三","phone":"18"}

2. Get 数据

(1) 接收单值

Gin框架中,Get请求的参数都保存在Context结构体中的queryCache字段中;
Gin框架中可以通过Query、DefaultQuery、GetQuery来获取Get参数信息,而Query、DefaultQuery是对GetQuery的二次封装, 而GetQuery的底层最终调用了GetQueryArray

  • gin源码

// 两个返回值的使用示例:
//     GET /?name=Manu&lastname=
//     ("Manu", true) == c.GetQuery("name")
//     ("", false) == c.GetQuery("id")
//     ("", true) == c.GetQuery("lastname")
底层调用 `GetQueryArray()`,返回 Array 的第[0]个值
func (c *Context) GetQuery(key string) (string, bool) {
   if values, ok := c.GetQueryArray(key); ok {
      return values[0], ok
   }
   return "", false
}

// 根据所得的 query key 返回一个string slice
底层调用 `initQueryCache()`
func (c *Context) GetQueryArray(key string) (values []string, ok bool) {
   c.initQueryCache()
   values, ok = c.queryCache[key]
   return
}

//将本次 context 中的请求报文的 url 数据赋给 queryCache 字段,完成初始化
func (c *Context) initQueryCache() {
   if c.queryCache == nil {
      if c.Request != nil {
         c.queryCache = c.Request.URL.Query()
      } else {
         c.queryCache = url.Values{}
      }
   }
}
  • 实例

  • 实例代码
func testReceiveGetParam( engine *gin.Engine)  {
	engine.GET("/receive", func(c *gin.Context) {
		// 如果不存在或为空,则返回:""
		`name := c.Query("name")`
		// 如果不存在或为空,则返回默认值
		`age := c.DefaultQuery("age","18")`
		// 直接使用GetQuery
		`home, ok := c.GetQuery("home")`
                
		c.PureJSON(200,gin.H{
			"msg":"success",
			"c.Query->name":name,
			"c.DefaultQuery->age":age,
			"c.GetQuery->home":home,
			"c.GetQuery->ok":ok,
		})
	})
}
  • 响应
➜ curl -X GET http://127.0.0.1:8080/receive?age=23&home=北京&name=小明
{"c.DefaultQuery->age":"23","c.GetQuery->ok":true,"c.GetQuery->home":"北京","c.Query->name":"小明","msg":"success"}

(2)接收数组

Gin框架中可以通过
QueryArray("param[]")GetQueryArray("param[]")
获取GET方式提交中的数组值信息,而QueryArray是对GetQueryArray二次封装

  • gin 源码

func (c *Context) QueryArray(key string) (values []string) {
   values, _ = c.GetQueryArray(key)
   return
}
  • 实例

  • 实例代码
//------ API 层 -------
// 接收数组
func TestReceiveGetArrayParam(engine *gin.Engine)  {
	engine.GET("/getArr", func(context *gin.Context) {
		// 接收GET数组:/getArr?name[]=张三&name[]=李四
		`nameList := context.QueryArray("name[]")`
                
		context.JSON(200,gin.H{
			"arr": nameList,
		})
	})
}
  • 响应
➜ curl -X GET http://127.0.0.1:8080/getArr?name[]=张三&name[]=李四
{ name:[张三,李四] }

(3)接收Map

Gin框架中可以通过
QueryMap("param")GetQueryMap("param")
获取GET方式提交中的map值信息,而QueryMap是对GetQueryMap二次封装

  • gin 源码

// QueryMap returns a map for a given query key.
func (c *Context) QueryMap(key string) (dicts map[string]string) {
   dicts, _ = c.GetQueryMap(key)
   return
}

// GetQueryMap returns a map for a given query key, plus a boolean value
// whether at least one value exists for the given key.
func (c *Context) GetQueryMap(key string) (map[string]string, bool) {
   c.initQueryCache()
   return c.get(c.queryCache, key)
}
  • 实例

  • 实例代码
// 接收map
func TestRecGetMapParam(engine *gin.Engine)  {
	engine.GET("/getMap", func(context *gin.Context) {
		//接收GET map:/getMap?score[语文]=95&score[数学]=100
		`queryMap := context.QueryMap("score")`
                
		context.JSON(200,gin.H{
			"map":queryMap,
		})
	})
}
  • 响应
➜ curl -X GET http://127.0.0.1:8080/getMap?score[语文]=95&score[数学]=100
{"map":{{语文:95},{数学:100}}}

3. POST 数据

Gin框架中,POST请求的参数都保存在Context结构体中的formCache字段中。
Gin框架中可以通过PostForm、DefaultPostForm、GetPostForm来获取Post提交的参数信息,而PostForm、DefaultPostForm同样是对GetPostForm的二次封装。

  • gin 源码

//获取表单
调用`GetPostFormArray`,获取Array[0]
func (c *Context) GetPostForm(key string) (string, bool) {
   if values, ok := c.GetPostFormArray(key); ok {
      return values[0], ok
   }
   return "", false
}

//获取表单数组
调用`initFormCache`
func (c *Context) GetPostFormArray(key string) (values []string, ok bool) {
   c.initFormCache()
   values, ok = c.formCache[key]
   return
}

// 初始化 formCache
func (c *Context) initFormCache() {
   if c.formCache == nil {
      c.formCache = make(url.Values)
      req := c.Request
      if err := req.ParseMultipartForm(c.engine.MaxMultipartMemory); err != nil {
         if !errors.Is(err, http.ErrNotMultipart) {
            debugPrint("error on parse multipart form array: %v", err)
         }
      }
      c.formCache = req.PostForm
   }
}
  • 实例

  • 实例代码
//传单个表单
func TestRecPostSingleValue(engine *gin.Engine) {
	engine.POST("/postSingle", func(context *gin.Context) {
		name := context.PostForm("name")
		age := context.DefaultQuery("age", "22")
		home, ok := context.GetPostForm("home") `
		context.JSON(200, gin.H{
			"postForm":         name,
			"DefaultQuery":     age,
			"GetPostForm.home": home,
			"GetPostForm.ok":   ok,
		})
	})
}
  • 响应
# 不传任何参数时,看接收情况
➜ curl -X POST http://127.0.0.1:8080/postSingle
{"DefaultQuery":"22","GetPostForm.home":"","GetPostForm.ok":false,"postForm":""}

# 传任何参数时,看接收情况
➜ curl -X POST http://127.0.0.1:8080/postSingle -d "age=40&home=南京&name=张三"
{"DefaultQuery":"22","GetPostForm.home":"南京","GetPostForm.ok":true,"postForm":"张三"}}
  • 实例代码
//传表单数组
func TestRecPostArrValue(engine *gin.Engine) {
	engine.POST("/postArr", func(context *gin.Context) {
		`arr := context.PostFormArray("name")`
                
		context.JSON(200, gin.H{
			"postArr": arr,
		})
	})
}
  • 响应
➜ curl -X POST http://127.0.0.1:8080/postArr -d "name[]=张三&name[]=李东"\
{"postArr":["张三","李东"]}

4. Header 数据

主要调用了Context中的Request来实现获取头部字段,
ContextRequest字段实际就是Go 标准库net/httpRequest struct

func (c *Context) GetHeader(key string) string {
	return c.requestHeader(key)
}
func (c *Context) requestHeader(key string) string {
	return c.Request.Header.Get(key)
} 

5. File 数据

Gin框架中,在处理File请求时,分为单个文件上传和多个文件上传 ,处理完成后再来实现文件的移动保存。

5-1 单文件上传

  • gin源码
func (c *Context) FormFile(name string) (*multipart.FileHeader, error) {
   if c.Request.MultipartForm == nil {
      if err := c.Request.ParseMultipartForm(c.engine.MaxMultipartMemory); err != nil {
         return nil, err
      }
   }
   f, fh, err := c.Request.FormFile(name)
   if err != nil {
      return nil, err
   }
   f.Close()
   return fh, err
}
  • 实例
// ------API 层------
func TestRecFile(engine *gin.Engine) {
	// 设置内存限制为8M, 默认是32MiB
	engine.MaxMultipartMemory = 8 << 20
	engine.POST("/file", func(c *gin.Context) {
		`file, err := c.FormFile("img")`
		if err != nil {
			c.JSON(500, gin.H{"err": err})
			return
		}
		// 文件重命名
		dst := "./tmp/"+file.Filename
		fmt.Println(dst)
		// 保存图片
		err = context.SaveUploadedFile(file, dst)
		if err != nil {
			c.JSON(500, gin.H{"err": "文件保存失败: " + err.Error()})
			return
		}
		c.JSON(200, gin.H{
			"msg":  "success",
			"name": file.Filename,
			"size": file.Size,
		})
	})
}

5-2 多文件上传

  • gin 源码
func (c *Context) MultipartForm() (*multipart.Form, error) {
   err := c.Request.ParseMultipartForm(c.engine.MaxMultipartMemory)
   return c.Request.MultipartForm, err
}
  • 实例
//---API 层 -------
// 接收多个文件
func TestRecFiles(engine *gin.Engine)  {
	// 设置内存限制为8M, 默认是32MiB
	engine.MaxMultipartMemory = 8 << 20
	engine.POST("/files", func(c *gin.Context) {
		// 接收图片
		`form, _ := c.MultipartForm()`
		files := form.File["imgList[]"]
		// 遍历保存
		for _, file := range files {
			_ = c.SaveUploadedFile(file, "./tmp/"+file.Filename)
		}
		context.String(200,"保存成功!")
	})
}

四、绑定请求数据

Gin框架中, 参数不但能通过指定key接收,也可以直接绑定到结构体中

1. 两种绑定方法

Gin提供了Must bind 和 Should bind两种类型的绑定方法,这两种类型对应的方法如下:

功能Must bind方法Should bind方法
BindShouldBind
绑定JSONBindJSONShouldBindJSON
绑定XMLBindXMLShouldBindXML
绑定GETBindQueryShouldBindQuery
绑定YAMLBindYAMLShouldBindYAML

MustBindWithShouldBindWith

Bind*类型的方法是对MustBindWith封装;如果MustBind绑定发生了错误,则请求终止,并响应400状态码;实际上MustBindWith在底层调用了ShouldBindWith并添加了错误 http 响应。

ShouldBind*类型的方法是对ShouldBindWith的封装。如果ShouldBind发生了绑定错误,Gin会返回错误并由开发者处理错误和请求。

2. 绑定 JSON

使用函数BindJSONShouldBindJSON来绑定提交的JSON参数信息。

  • Model 层
    1. struct 的每个字段的tag添加json:"jsonName"
  • API 层
    1. 定义一个 Struct 实例对象 s
    2. s的引用传入 c.ShouldBindJSON(&s) , http 请求中的JSON数据将注入到 s

2-1 代码

//------ Model 层 -------
// 定义待绑定的JSON结构体
type Param struct {
    Name string `json:"name"`
    Age int `json:"age"`
    Likes []string `json:"likes"`
}

//------ API 层 -------
func TestBindJson(engine *gin.Engine) {
    engine.POST("/bindJson", func(context *gin.Context) {
    var jsonParam Param
    var err error
    bindType := context.Query("type")
    fmt.Println(bindType)
    if bindType == "1" {
        err = context.BindJSON(&jsonParam)
    } else {
        err = context.ShouldBindJSON(&jsonParam)
    }
    if err != nil {
        context.JSON(500, gin.H{"error": err})
        return
        }
    context.JSON(200, gin.H{"result": jsonParam})
    })
}

3. 绑定 URL 参数

通过使用函数BindQueryShouldBindQuery,用来只绑定GET请求中的uri参数,如:/funcName?a=x&b=x中的a和b

3-1 代码


//------ Model 层 -------
type UriParam struct {
    Name string `form:"name" binding:"required"`
    Age int `form:"age"`
    Home string `form:"home"`
}

//------ API 层 -------
func TestBindQuery(engine *gin.Engine) {
    engine.GET("/bindQuery", func(context *gin.Context) {
    bindType := context.Query("type")
    var uriParam UriParam
    var err error
    if bindType == "1" {
        fmt.Println("BindQuery")
        err = context.BindQuery(&uriParam)
    } else {
    fmt.Println("ShouldBindQuery")
    err = context.ShouldBindQuery(&uriParam)
    }
    if err != nil {
        context.JSON(500, gin.H{"error": err.Error()})
        return
    }
    fmt.Printf("uriParam:%+v\n", uriParam)
    context.JSON(200, gin.H{"result": uriParam})
    })
}

3-2 请求

Bash
# 参数都传
➜ curl -X GET http://127.0.0.1:8080/bindQuery?age=24&name=张三&home=北京&type=1
{"result":{"Name":"张三","Age":24,"Home":"北京"}}
➜ curl -X GET http://127.0.0.1:8080/bindQuery?age=24&name=张三&home=北京&type=2
{"result":{"Name":"张三","Age":24,"Home":"北京"}}
# 必填参数name不填时,都会报错
➜ curl -X GET http://127.0.0.1:8080/bindQuery?age=24&home=北京&type=2
{"error":"Key: 'UriParam.Name' Error:Field validation for 'Name' failed on the 'required' tag"}

五、响应数据

Gin支持这几种渲染:JSON,IndentedJson,SecureJSON,XML,StringDataRedirect,HTML,HTMLDebug,HTMLProduction,YAML,Reader, ProtoBuf,AsciiJSON
它们都实现了Render接口中的Render;也实现了Header的写入。

// JSON 序列化了传入的 object ,转换为 JSON 格式传到响应体中
// 同时也将 Content-Type 设为 "application/json".
func (c *Context) JSON(code int, obj any) {
   c.Render(code, render.JSON{Data: obj})
}


// Render 写入到响应头,并调用 render.Render 来渲染数据.
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)
   }
}

参考