在上一篇文章深入gin框架内幕(一)中,主要介绍了Gin框架中是如何创建一个HTTP服务以及内部的核心结构和常用的一些结构体方法,并在最后以一个简单的示例来详细讲解Gin框架内部具体是如何运行的,但是在最后我们会发现使用了一个
Context
引用对象的一些方法来返回具体的HTTP响应数据,在本篇文章中,我们将继续学习和分析Gin框架内幕。
在开始分析之前,我们先简单回顾一下上一个章节中讲到的Gin框架中的几个核心的结构.
Gin框架中的几个重要的模型:
Engine
: 用来初始化一个gin
对象实例,在该对象实例中主要包含了一些框架的基础功能,比如日志,中间件设置,路由控制(组),以及handlercontext等相关方法.源码文件Router
: 用来定义各种路由规则和条件,并通过HTTP服务将具体的路由注册到一个由context实现的handler中Context
是框架中非常重要的一点,它允许我们在中间件间共享变量,管理整个流程,验证请求的json以及提供一个json的响应体. 通常情况下我们的业务逻辑处理也是在整个Context引用对象中进行实现的.Bind
相关的结构方法来解析context中的HTTP数据我们在深入Gin框架内幕(一)中,以一个简单的Gin实例来具体讲解它内部是如何创建一个Http服务,并且注册一个路由来接收用户的请求,在示例程序中我们使用了Context
引用对象的String
方法来处理HTTP服务的数据响应,所以在整个Gin框架中紧跟Router
模型结构的就要属Context
结构了,该结构体主要用来处理整个HTTP请求的上下文数据,也是我们在开发HTTP服务中相对比较重要的一个结构体了。
# 深入Gin框架内幕(一)中的示例
$ cat case1.go
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
ginObj := gin.Default()
ginObj.Any("/hello",func(c *gin.Context){
c.String(http.StatusOK,"Hello BGBiao.")
})
ginObj.Run("localhost:8080")
}
我们可以看到,在使用Gin框架后,我们只需要很简单的代码,即可以快速运行一个返回Hello BGBiao.
的HTTP服务,而在ginObj.Any
方法中,我们传入了一个参数为Context
引用类型的匿名函数,并在该函数内部采用String(code,data)
方法来处理HTTP服务的响应数据(返回Hello BGBiao字符串),这个时候,你可能会想,我们在企业内部都是前后端分离,通常情况下后端仅会提供RESTful API
,并通过JSON
格式的数据和前端进行交互,那么Gin是如何处理其他非字符串类型的数据响应呢,这也是我们接下来要主要讲的Context
结构模型。
注意:
在Gin框架中由Router
结构体来负责路由和方法(URL和HTTP方法)的绑定,内的Handler采用Context
结构体来处理具体的HTTP数据传输方式,比如HTTP头部,请求体参数,状态码以及响应体和其他的一些常见HTTP行为。
Context结构体
:
type Context struct {
// 一个包含size,status和ResponseWriter的结构体
writermem responseWriter
// http的请求体(指向原生的http.Request指针)
Request *http.Request
// ResonseWriter接口
Writer ResponseWriter
// 请求参数[]{"Key":"Value"}
Params Params
handlers HandlersChain
index int8
// http请求的全路径地址
fullPath string
// gin框架的Engine结构体指针
engine *Engine
// 每个请求的context中的唯一键值对
Keys map[string]interface{}
// 绑定到所有使用该context的handler/middlewares的错误列表
Errors errorMsgs
// 定义了允许的格式被用于内容协商(content)
Accepted []string
// queryCache 使用url.ParseQuery来缓存参数查询结果(c.Request.URL.Query())
queryCache url.Values
// formCache 使用url.ParseQuery来缓存PostForm包含的表单数据(来自POST,PATCH,PUT请求体参数)
formCache url.Values
}
Context结构体常用的一些方法
基本方法
:
Handler()
: 返回当前的主handler(func (c *Context) Handler() HandlerFunc
)http常用方法
:
X-Real-IP
,X-Forwarded-For
)流控相关的方法:
错误管理
:
元数据管理
:
输入数据
:
Bind家族相关方法
:
Content-Type
并绑定到指定的binding引擎binding.JSON
binding.Uri
来绑定传递的结构体指针ShouldBind家族相关方法
:
HTTP响应相关的方法
:
c.Writer.Header().Set(key, value)
的简单实现,在响应体重写入一个header,如果value为空,则相当于调用了c.Writer.Header().Del(key)
Context.JSON()
) Secure Json
xml
格式,并写Content-Type:"application/xml"yaml
probuf
3.1返回json格式的数据
为了解决我们在开头提到的问题,我们将使用context引用对象的JSON家族方法来处理该需求
# 使用context来返回json格式的数据
$ cat case2.go
package main
import (
"github.com/gin-gonic/gin"
)
// 我们定义一个通用的格式化的响应数据
// 在Data字段中采用空接口类型来实际存放我们的业务数据
type restData struct {
Data interface{} `json:"data"`
Message string `json:"message"`
Status bool `json:"status"`
}
func main() {
// mock一个http响应数据
restdata := &restData{"Hello,BGBiao","",true}
restdata1 := &restData{map[string]string{"name":"BGBiao","website":"https://bgbiao.top"},"",true}
// 使用Gin框架启动一个http接口服务
ginObj := gin.Default()
ginObj.GET("/api/test",func(c *gin.Context){
// 我们的handlerFunc中入参是一个Context结构的引用对象c
// 因此我们可以使用Context中的JSON方法来返回一个json结构的数据
// 可用的方法有如下几种,我们可以根据实际需求进行选择
/*
IndentedJSON(code int, obj interface{}): 带缩进的json(消耗cpu和mem)
SecureJSON(code int, obj interface{}): 安全化json
JSONP(code int, obj interface{})
JSON(code int, obj interface{}): 序列化为JSON,并写Content-Type:"application/json"头
*/
c.JSON(200,restdata)
})
ginObj.GET("/api/test1",func(c *gin.Context){
c.IndentedJSON(200,restdata1)
})
ginObj.Run("localhost:8080")
}
# 实例运行(这里成功将我们写的两个api接口进行对外暴露)
$ go run case2.go
....
....
[GIN-debug] GET /api/test --> main.main.func1 (3 handlers)
[GIN-debug] GET /api/test1 --> main.main.func2 (3 handlers)
# 接口测试访问
$ curl localhost:8080/api/test
{"data":"Hello,BGBiao","message":"","status":true}
$ curl localhost:8080/api/test1
{
"data": {
"name": "BGBiao",
"website": "https://bgbiao.top"
},
"message": "",
"status": true
}%
当然上面我们仅以JSON格式来示例,类似的方式我们可以使用XML
,YAML
,ProtoBuf
等方法来输出指定格式化后的数据。
3.2其他常用的基本方法
注意:
在其他基本方法中我们仍然使用上述示例代码中的主逻辑,主要用来测试基本的方法.
# 我们在/api/test这个路由中增加如下两行代码
// 设置响应体中的自定义header(通常我们可以通过自定义头来实现一个内部标识)
c.Header("Api-Author","BGBiao")
// GetHeader方法用来获取指定的请求头,比如我们经常会使用请求中的token来进行接口的认证和鉴权
// 这里由于我们使用的restdata的指针,通过GetHeader方法获取到token赋值给Message
// ClientIP()方法用于获取客户端的ip地址
restdata.Message = fmt.Sprintf("token:%s 当前有效,客户端ip:%s",c.GetHeader("token"),c.ClientIP())
# 访问接口示例(我们可以看到在响应体中多了一个我们自定义的Api-Author头,并且我们将请求头token的值)
$ curl -H 'token:xxxxxxxx' localhost:8080/api/test -i
HTTP/1.1 200 OK
Api-Author: BGBiao
Content-Type: application/json; charset=utf-8
Date: Sun, 12 Jan 2020 14:41:01 GMT
Content-Length: 66
{"data":"Hello,BGBiao","message":"token:xxxxxxxx 当前有效,客户端ip:127.0.0.1","status":true}
3.3用户数据输入
当然到这里后,你可能还会有新的疑问,就是通常情况下,我们开发后端接口会提供一些具体的参数,通过一些具体数据提交来实现具体的业务逻辑处理,这些参数通常会分为如下三类:
以上的基本需求,几乎都可以在Context结构体的输入数据
中找到响应的方法.
# 接下来,我们依然在上述的代码中进行修改,增加如下路由
$ cat case2.go
....
....
// 比如我们该接口时用来获取全部数据,但是我们希望在url中增加参数来限制数据条数
datas := []string{"Golang","Python","Docker","Kubernetes","CloudNative","DevOps"}
ginObj.GET("/api/testdata",func(c *gin.Context){
limit := c.Query("limit")
// 其实既然这里我们已经确定需求了,当用户没有输入limit参数时我们就可以设置默认值
// DefaultQuery("limit","1")
// 同时我们其实也可以使用GetQuery方法来获取参数解析状态,即是否有对应的参数
// 还有QueryArray和GetQueryArray类似的方法
if limit != "" {
num,_ := strconv.Atoi(limit)
restdata1.Data = datas[:num]
}else {
restdata1.Data = datas
}
c.IndentedJSON(200,restdata1)
})
// 使用form表单方式提交数据
ginObj.POST("/api/testdata",func(c *gin.Context){
// 使用c.PostForm方法来提交一个data数据
// 同时我们可以使用DefaultPostForm方法来给提交数据一个默认值,比如我们有些参数是希望有默认值的
// 当然也可以使用GetPostForm,PostFormArray,PostFormArray方法来获取多个数据和状态
// data := c.PostForm("data")
// datas = append(datas,data)
/* 这里可能会有个问题就是同时提交多个数据时,使用PostForm方法就会不那么好使了
通常情况下回使用PostFormArray方法
*/
data := c.PostFormArray("data")
datas = append(datas,data...)
restdata1.Data = datas
c.IndentedJSON(200,restdata1)
})
// 获取url中的路径参数
ginObj.GET("/api/testdata/:data",func(c *gin.Context){
data := c.Param("data")
for _,rawData := range datas {
if data == rawData {
restdata1.Data = data
break
}
}
if restdata1.Data != data {
restdata1.Data = ""
restdata1.Message = fmt.Sprintf("%v 不存在",data)
restdata1.Status = false
}
c.IndentedJSON(200,restdata1)
})
....
....
# 请求示例接口
# 我们可以看到使用GET方法默认会获取到全部数据,但是如果有了limit参数后,我们就可以限制数据的条数
$ curl -H 'token:xxxxxxxx' localhost:8080/api/testdata
{
"data": [
"Golang",
"Python",
"Docker",
"Kubernetes",
"CloudNative",
"DevOps"
],
"message": "",
"status": true
}%
$ curl -H 'token:xxxxxxxx' "localhost:8080/api/testdata?limit=2"
{
"data": [
"Golang",
"Python"
],
"message": "",
"status": true
}%
# 当我们使用post接口往服务提交数据时,就可以让服务端按照需求进行数据处理
curl -X POST -d data="vue" "localhost:8080/api/testdata"
{
"data": [
"Golang",
"Python",
"Docker",
"Kubernetes",
"CloudNative",
"DevOps",
"vue"
],
"message": "",
"status": true
}%
# 当我们同时需要提交多份数据时,可以使用PostFormArray方法,同时提交多份数据(可以理解为批量提交)
$ curl -X POST -d data="vue" -d data="Rust" "localhost:8080/api/testdata"
# 获取URL中的参数值
$ curl "localhost:8080/api/testdata/Golang"
{
"data": "Golang",
"message": "",
"status": true
}%
$ curl "localhost:8080/api/testdata/Java"
{
"data": "",
"message": "Java 不存在",
"status": false
}%