Gin 框架

Gin框架介绍

package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"net/http"
	"strconv"
)

// 运行 go run main.go
// 模拟请求: curl localhost:8080/someGet/aaa
//			curl -X POST localhost:8080/hello
//			curl -X TRACE localhost:8080/hello

/*func main() {
	// 初始化一个gin 实例,内部调用New() 函数来初始化一个gin 实例 // 同时使用Use(middleware ...HandlerFunc) IRoutes 方法注册了Logger和Recovery两个中间件
	// 在New()初始化gin实例的过程中还默认初始化了一个bathPath为"/"的RouterGroup,其实就是一个router实例
	router := gin.Default()

	// 此规则能够匹配/someGet/aaa这种格式,但不能匹配/usomeGet/ 或 /someGet这种格式
	router.GET("/someGet/:name", getting)

	// 但是,这个规则既能匹配/someGet/aaa/格式也能匹配/someGet/aaa/send这种格式
	// 如果没有其他路由器匹配/someGet/aaa,它将重定向到/someGet/aaa/
	router.GET("/someGet/:name/*action", func(c *gin.Context) {
		name := c.Param("name")
		action := c.Param("action")
		message := name + " is " + action
		c.String(http.StatusOK, message)
	})

	// Any() 方法会将HTTP 所有方法都注册上去
	router.Any("/hello", func(c *gin.Context) {
		c.String(http.StatusOK, "hello yang") // 采用String(code,data) 方法来处理HTTP服务的相应数据
	})

	// 当所有的路由注册完成后,可以使用gin 的结构体方法来实际运行HTTP 服务,以接收用户的http 请求,该方法除非出现错误,否则会无期限阻塞调用goroutine来接收请求
	router.Run("localhost:8080")
}*/

func getting(c *gin.Context) {
	name := c.Param("name")
	c.String(http.StatusOK, "Hello %s", name)
}

// 通常后端通过JSON 格式的数据和前端进行交互,
// Gin 是如何处理其他非字符串类型的数据相应呢,---Context:*gin.Context

// gin 框架中由Router 结构体来负责路由和方法(URL和HTTP方法)的绑定。其中的Handler采用Context 结构体来处理具体的HTTP数据传输方式,比如HTTP 头部,请求体参数,状态码记忆响应体和其他一些常见的HTTP行为。
// Context 结构体常用的一些方法
// Copy():返回当前正在使用的context的拷贝(context指针),当这个context必须在goroutine中用时,该方法比较有用
// Handler(): 返回当前的主handler(func (c *Context) Handler() HandlerFunc)
// ClientIP() string: 返回客户端ip(该方法会解析X-Real-IP,X-Forwarded-For)
// Next(): 该方法仅被使用在middleware中,它会在被调用的handler链内部执行pending handler
// IsAborted(): 如果当前的context被终止了,该方法返回true
// Set(key string, value interface{}): 给当前这个context设置一个新的键值对
// Get(key string) (value interface{}, exists bool): 返回指定的key的值,以及是否存在
// Param(key string) string: 返回URL的参数值(uri_patten: "/user/:id",url: "/user/john",c.Param("id") = "john")
// Query(key string) string: 返回url中的查询参数值(url: "/path?id=1234&name=Manu&value=",c.Query("id")为1234,c.Query("name")为Manu,c.Query("value")为空)
// Bind(obj interface{}) error: 自动解析Content-Type并绑定到指定的binding引擎
// BindJSON(obj interface{}) error: 同上,binding引擎为binding.JSON
// ShouldBind(obj interface{}) error: 同上述的Bind()方法,但是该方法在json结构无效时不会返回400
// Status(code int): 设置http的响应码
// SetCookie(name, value string, maxAge int, path, domain string, secure, httpOnly bool): 该方法将设置一个Set-Cookie到ResponseWriter的头中(注意:name必须是一个合法可用的名称,无效的coookie可能会被丢弃)
// Cookie(name string) (string, error): 返回名称为name的cookie
// Render(code int, r render.Render): 该方法写入响应头并调用render.Render去渲染数据
// DataFromReader(code int, contentLength int64, contentType string, reader io.Reader, extraHeaders map[string]string): 写一些制定模板的数据到响应体中,并更新状态码

type restData struct {
	Data    interface{} `json:"data"`
	Message string      `json:"message"`
	Status  bool        `json:"status"`
}

func main() {
	// mock 一个 http 响应数据
	restdata := &restData{"Hello","",true}
	restdata1 := &restData{map[string]string{"name":"yang","sex":"girl"},"",true}

	// 使用Gin 框架启动一个http 接口服务
	route := gin.Default()
	route.GET("/api/test", func(c *gin.Context) {

		// 新增2行代码
		// 设置响应体中的自定义header(通常可以通过自定义头来实现一个内部标识)
		c.Header("Api-Author","yang")
		// GetHeader 方法用来获取指定的请求头,比如经常使用请求中的token 来进行接口的认证和鉴权
		// 这里由于的是使用restdata 指针,通过GetHeader 方法获取到token赋值给Message
		// ClientIP() 方法用于获取客户端的ip地址
		restdata.Message = fmt.Sprintf("token:%s 当前有效,客户端IP:%s",c.GetHeader("token"),c.ClientIP())


		// handlerFunc 中入参是一个JSON方法来返回一个json 结构的数据
		// 因此可以使用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)
	})

	route.GET("/api/test1", func(c *gin.Context) {
		c.IndentedJSON(200,restdata1)
	})


	// 新增以下接口:
	// 用户数据输入:
	// 比如该接口用来获取全部数据,但是希望在url 中增加参数来限制数据条数
	datas := []string{"Golang","python","Docker","Kubernetes","cloudNative","DevOps"}
	route.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 表单方式提交数据
	route.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 中的路径参数
	route.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)
	})



	route.Run("localhost:8080")
}


// 接口测试访问:curl localhost:8080/api/test
/*
curl localhost:8080/api/test
{"data":"Hello","message":"","status":true}%
*/
// 接口测试访问: curl localhost:8080/api/test1
/*
 curl localhost:8080/api/test1
{
    "data": {
        "name": "yang",
        "sex": "girl"
    },
    "message": "",
    "status": true
}%
*/

//当然上面我们仅以JSON格式来示例,类似的方式我们可以使用XML,YAML,ProtoBuf等方法来输出指定格式化后的数据。


// 当新增Header 和 ClientIP 后,访问接口实例:可以看到响应体中多了一个自定义的Api-Author 头
/*
curl -H 'token:xxx' localhost:8080/api/test -i
HTTP/1.1 200 OK
Api-Author: yang
Content-Type: application/json; charset=utf-8
Date: Tue, 10 Nov 2020 12:55:56 GMT
Content-Length: 91

{"data":"Hello","message":"token:xxx 当前有效,客户端IP:127.0.0.1","status":true}%
*/


// 新增以下方法后:
// 请求实例接口:可以看到使用GET 方法默认会获取到全部数据,但是如果有了limit 参数后,就可以限制数据的条数
/*
 curl -H 'token:xxx' localhost:8080/api/testdata
{
    "data": [
        "Golang",
        "python",
        "Docker",
        "Kubernetes",
        "cloudNative",
        "DevOps"
    ],
    "message": "",
    "status": true
}%
*/
/*
$ curl -H 'token:xxx' "localhost:8080/api/testdata?limit=2"
{
    "data": [
        "Golang",
        "python"
    ],
    "message": "",
    "status": true
}%
*/

//当我们使用post接口往服务提交数据时,就可以让服务端按照需求进行数据处理
/*
$ curl -H 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"
/*
$ curl -X POST -d data="vue" -d data="Rust" "localhost:8080/api/testdata"
{
    "data": [
        "Golang",
        "python",
        "Docker",
        "Kubernetes",
        "cloudNative",
        "DevOps",
        "vue",
        "vue",
        "Rust"
    ],
    "message": "",
    "status": true
}%
*/

//获取URL中的参数值
/*
$ curl "localhost:8080/api/testdata/Golang"
{
    "data": "Golang",
    "message": "",
    "status": true
}%
*/

/*
$ curl "localhost:8080/api/testdata/aaa"
{
    "data": "",
    "message": "aaa 不存在",
    "status": false
}%
*/

Gin是一个用 Go (Golang) 编写的 web 框架。它是一个类似于martini但性能更好的API框架,不同于谢大主导的Beegoweb框架,后者更像是Python语言中的Django框架,内部包含了开发一个web程序所需的各种组件。

如果你是性能和高效的追求者,我相信你会像我一样爱上Gin。

同时,不同于其他Golang语言的API框架,该框架社区十分活跃,并且主程仍然在不断更新和改进,我们知道通常情况下在选择一个开源的软件或者相关库时,社区的活跃度以及项目的更新情况会非常重要(考虑到后期的维护和性能和特性问题)。

另外一方面,该框架官方提供了很多简单的示例来供我们快速运行一个期望的http服务,这对于一个刚转入Golang进行业务开发的同学来说是一个非常好的开始。

Gin官方提供的各种HTTP服务示例

当然我在很长一段时间也仅是去看官方示例来快速熟悉并实现自己的业务需求,但当有一些特殊的需求时通常去查看官方的具体实现来满足需求,长期如此,不仅耗时且效率极低,因此我产生了将核心源码探究一番的兴趣,希望能通过源码方式来对Gin框架有个深入的学习。

Gin框架中的几个核心结构
我们都知道开发一个HTTP服务,首先需要启动一个TCP监听,然后需要有一些列的handler来处理具体的业务逻辑,最后在再将具体的业务逻辑通过HTTP协议约定和相关的Method和URL进行绑定,以此来对外提供具体功能的HTTP服务。那么在Gin框架对应的就是如下几个模型,我们将一起学习Gin的实现。

Gin框架中的几个重要的模型:

  • Engine: 用来初始化一个gin对象实例,在该对象实例中主要包含了一些框架的基础功能,比如日志,中间件设置,路由控制(组),以及handlercontext等相关方法.源码文件
  • Router: 用来定义各种路由规则和条件,并通过HTTP服务将具体的路由注册到一个由context实现的handler中
  • Context: Context是框架中非常重要的一点,它允许我们在中间件间共享变量,管理整个流程,验证请求的json以及提供一个json的响应体. 通常情况下我们的业务逻辑处理也是在整个Context引用对象中进行实现的.
  • Bind: 在Context中我们已经可以获取到请求的详细信息,比如HTTP请求头和请求体,但是我们需要根据不同的HTTP协议参数来获取相应的格式化数据来处理底层的业务逻辑,就需要使用Bind相关的结构方法来解析context中的HTTP数据

1.Gin框架中的Engine结构体
当我们在使用框架[Gin](https://github.com/gin-gonic/gin)来创建一个HTTP服务时,首先我们需要初始化一个实例,在Engine结构体中就包含了实例的一些基本属性和实例化的一些方法。

Engine结构体:

type Engine struct {
    // 路由组,在实际开发过程中我们通常会使用路由组来组织和管理一些列的路由. 比如: /apis/,/v1/等分组路由
    RouterGroup
    // 开启自动重定向。如果当前路由没有匹配到,但是存在不带/开头的handler就会重定向. 比如: 用户输入/foo/但是存在一个/foo 就会自动重定向到该handler,并且会向客户端返回301或者307状态码(区别在于GET方法和其他方法)
    RedirectTrailingSlash bool
    // 如果开启该参数,没有handler注册时,路由会尝试自己去修复当前的请求地址. 
    // 修复流程:
    // 1.首位多余元素会被删除(../ or //); 2.然后路由会对新的路径进行不区分大小写的查找;3.如果能正常找到对应的handler,路由就会重定向到正确的handler上并返回301或者307.(比如: 用户访问/FOO 和 /..//Foo可能会被重定向到/foo这个路由上)
    RedirectFixedPath bool
    // 如果开启该参数,当当前请求不能被路由时,路由会自己去检查其他方法是否被允许.在这种情况下会响应"Method Not Allowed",并返回状态码405; 如果没有其他方法被允许,将会委托给NotFound的handler
    HandleMethodNotAllowed bool
    // 是否转发客户端ip
    ForwardedByClientIP    bool
    // 如果开启将会在请求中增加一个以"X-AppEngine..."开头的header
    AppEngine bool
    // 如果开启将会使用url.RawPath去查找参数(默认:false)
    UseRawPath bool
    // 如果开启,请求路径将不会被转义. 如果UseRawPath为false,该参数实际上就为true(因为使用的是url.Path)
    UnescapePathValues bool
    // maxMemory参数的值(http.Request的ParseMultipartForm调用时的参数)
    MaxMultipartMemory int64
    // 是否删除额外的反斜线(开始时可解析有额外斜线的请求)
    RemoveExtraSlash bool
    // 分隔符(render.Delims表示使用HTML渲染的一组左右分隔符,具体可见html/template库)
    delims           render.Delims
    // 设置在Context.SecureJSON中国的json前缀
    secureJsonPrefix string
    // 返回一个HTMLRender接口(用于渲染HTMLProduction和HTMLDebug两个结构体类型的模板)
    HTMLRender       render.HTMLRender
    // html/template包中的FuncMap map[string]interface{} ,用来定义从名称到函数的映射
    FuncMap          template.FuncMap
    // 以下是gin框架内部定义的一些属性 
    // HandlersChain 是一个HandlerFunc 的数组(HandlerFunc其实就是一个Context的指针,Context会在下一节讲解)
    allNoRoute       HandlersChain
    allNoMethod      HandlersChain
    noRoute          HandlersChain
    noMethod         HandlersChain
    // 这里定义了一个可以临时存取对象的集合(sync.Pool是线程安全的,主要用来缓存为使用的item以减少GC压力,使得创建高效且线程安全的空闲队列)
    pool             sync.Pool
    // methodTrees是methodTree的切片(methodTree是一个包含请求方法和node指针的结构体,node是一个管理path的节点树)
    trees            methodTrees
}

HandlerFunc定义:

// 定义了一个可以被中间件使用的handler
type HandlerFunc func(*Context)

初始化Engine的方式:

  • New(): 该函数返回一个默认的Engine引用实例(开启了自动重定向,转发客户端ip和禁止请求路径转义)
  • Default(): 内部调用New()函数,但是增加了Logger和Recovery两个中间件

Engine对外常用的方法:

  • Delims(left, right string) *Engine: 给创建好的gin实例指定模板引擎的左右分割符
  • SecureJsonPrefix(prefix string) *Engine: 给创建好的gin实例设置secureJsonPrefixi
  • SetHTMLTemplate(templ *template.Template): 该方法会gin实实例绑定一个模板引擎(内部其实是设置了engine的HTMLRender属性)
  • LoadHTMLGlob(pattern string): 该方法用来加载glob模式(类似于shell中的正则)的html模板文件,然后将结果和HTML模板引擎关联(内部调用SetHTMLTemplate方法将全部匹配到模板注册进去)
  • LoadHTMLFiles(files …string): 该方法用上,需要指定一组模板文件名
  • SetFuncMap(funcMap template.FuncMap): 该方法会设置一个FuncMap给template.FuncMap使用(内部其实设置了engine的FuncMap)
  • NoRoute(handlers …HandlerFunc): 该方法为NoRoute增加一些handler,它默认会返回404(通常在企业里,404我们会处理的比较优雅一些,比如给一些企业的静态页啥的)
  • NoMethod(handlers …HandlerFunc): 同上,该方法用于给NoMethod增加handler,默认返回405
  • Use(middleware …HandlerFunc) IRoutes: 该方法用于绑定一个全局的中间件给router. 通过该方法注册的中间件将包含在每个请求的handler chain中(比如可以在这里使用一些logger或者error相关的中间件). 在上面初始化实例的Default()函数中其实内部使用了engine.Use(Logger(), Recovery())来加载logger和recovery中间件
  • Routes() (routes RoutesInfo): 该方法用来返回一个路由列表信息RoutesInfo(一个路由信息RouteInfo中包含Method,Path,Handler,HandlerFunc),该方法底层调用engine的trees来获取一些router必要的信息.
  • Run(addr …string) (err error): 该方法会绑定router到http.Server中并开启一个http监听来接收http请求. 该方法其实是http.ListenAndServe(addr, engine)的简单实现. 注意:该方法除非出现错误,否则会无期限阻塞调用goroutine来接收请求(engine内部只要实现了http.ServeHTTP方法即可)
  • RunTLS(addr, certFile, keyFile string) (err error): 同上,以https方式运行服务
  • RunUnix(file string) (err error): 同Run(addr)方法,通过指定的unix socket文件运行服务
  • RunFd(fd int) (err error): 同Run(addr)方法,通过指定的文件描述符(fd)来运行服务
  • RunListener(listener net.Listener) (err error): 同Run(addr),通过制定的net.Listener来运行服务
  • ServeHTTP(w http.ResponseWriter, req *http.Request): 该方法遵循了http.Handler的接口规范,可使gin内部调用http.ListenAndServe来启动一个http服务
  • HandleContext(c *Context): 该方法会重新确认一个被重写的context(可以通过c.Request.URL.Path来实现). 需要注意的是该方法可能造成context的循环使用(会绕死你,谨慎使用)

2.Gin框架中的Router
使用Engine结构体中提供的相关方法,我们就可以快速的启动一个HTTP服务了,但是如何对外暴露一个URL来简单实现一个HTTP的数据传输呢,这个时候就需要使用Router中的方法了。

Gin框架中Router相关的结构体:

  • RouterGroup: 该结构体被用来在Gin内部配置一个路由,一个RouterGroup被用来关联URL前缀和一组具体的handler业务逻辑
  • IRoutes: IRoutes是一个定了了所有路由处理的接口(包含一些常用的HTTP方法)
  • IRouter: IRouter则是一个包含单个路由和路由组的所有路由处理的接口
    RouterGroup相关结构定义:

// RouterGroup 结构体

type RouterGroup struct {
	Handlers HandlersChain
	basePath string
	engine   *Engine
	root     bool
}

// IRoutes 接口

type IRoutes interface {
	Use(...HandlerFunc) IRoutes

	Handle(string, string, ...HandlerFunc) IRoutes
	Any(string, ...HandlerFunc) IRoutes
	GET(string, ...HandlerFunc) IRoutes
	POST(string, ...HandlerFunc) IRoutes
	DELETE(string, ...HandlerFunc) IRoutes
	PATCH(string, ...HandlerFunc) IRoutes
	PUT(string, ...HandlerFunc) IRoutes
	OPTIONS(string, ...HandlerFunc) IRoutes
	HEAD(string, ...HandlerFunc) IRoutes

	StaticFile(string, string) IRoutes
	Static(string, string) IRoutes
	StaticFS(string, http.FileSystem) IRoutes
}

// IRouter接口

type IRouter interface {
	IRoutes
	Group(string, ...HandlerFunc) *RouterGroup
}

还记得在上一节中我们的Engine结构体中有一个RouterGroup字段吗,该字段会在我们创建一个Engine的gin实例后帮助我们初始化一个默认的RouterGroup实例。

比如在Engine结构体的中的New()函数,会初始化一个带有如下RouterGroup的gin实例,并将gin实例注册到RouterGroup的engine字段.源码文件

// https://github.com/gin-gonic/gin/blob/master/gin.go#L129
		RouterGroup: RouterGroup{
			Handlers: nil,
			basePath: "/",
			root:     true,
		},

RouterGroup结构体对外暴露的常用方法:

  • Use(middleware …HandlerFunc) IRoutes: 注册一个中间件并返回Iroutes接口
  • Group(relativePath string, handlers …HandlerFunc) *RouterGroup: Group方法会创建一个新的路由组。通常我们会创建一个公共的中间件或者是具有相同前缀的路由,来归并到一个路由组
  • BasePath() string: 该方法用来返回一个路由组初始路径(比如 v := router.Group(“/rest/n/v1/api”),则v.BasePath()就是”/rest/n/v1/api”)
  • Handle(httpMethod, relativePath string, handlers …HandlerFunc) IRoutes: 该方法会使用给定的HTTP方法和URL来注册一个新的handler。(最后一个handler应该是真正的处理程序,其他的应该是在不同的路由之间共享的中间件)。注意:内部调用了一个handle(httpMethod, relativePath string, handlers HandlersChain)的私有方法来处理核心逻辑
  • POST(relativePath string, handlers …HandlerFunc) IRoutes: 该方法是router.Handle(“POST”, path, handle)的快速实现,
  • GET(relativePath string, handlers …HandlerFunc) IRoutes: 同上
  • DELETE(relativePath string, handlers …HandlerFunc) IRoutes: 同上
  • PATCH(relativePath string, handlers …HandlerFunc) IRoutes: 同上
  • PUT(relativePath string, handlers …HandlerFunc) IRoutes: 同上
  • OPTIONS(relativePath string, handlers …HandlerFunc) IRoutes: 同上
  • HEAD(relativePath string, handlers …HandlerFunc) IRoutes: 同上
  • Any(relativePath string, handlers …HandlerFunc) IRoutes: 同上,会将HTTP的所有方法都注册上去
  • StaticFile(relativePath, filepath string) IRoutes: 该方法用来注册一台路由来服务本地文件系统的单个文件,比如:router.StaticFile(“favicon.ico”, “./resources/favicon.ico”)
  • Static(relativePath, root string) IRoutes: 该方法用来提供一个指定文件系统根路径的的路由,内部调用group.StaticFS(path,Dir(root,false))来提供服务
  • StaticFS(relativePath string, fs http.FileSystem) IRoutes: 指定文件系统(http.FileSystem)来创建一个服务

3.Gin实例示例
有了上面两个核心模型Engine和RouteGroup的了解,此时我们就可以通过Gin框架快速来创建一个简单HTTP服务了。

1.默认路由

# 测试示例
$ cat case1.go
package main

import (
    "net/http"

    "github.com/gin-gonic/gin"
)

func main() {
    // 使用Default()函数来初始化一个gin实例(engine结构体的引用对象)
    // Default函数内部调用New()函数来初始化一个gin实例,
    // 同时使用Use(middleware ...HandlerFunc) IRoutes 方法注册了Logger和Recovery两个中间件
    // 在New()初始化gin实例的过程中还默认初始化了一个bathPath为"/"的RouterGroup,其实就是一个router实例
    ginObj := gin.Default()
    // 由于RouterGroup在engine结构体中是一个匿名对象,因此实例化的engine引用对象就可以直接操作RouterGroup结构体里对外暴露的所有方法
    // 这里我们尝试注册一个包含所有HTTP方法的路由
    // https://github.com/gin-gonic/gin/blob/master/routergroup.go#L133
    // 而在RouterGroup的各种对外暴露的方法中,底层调用了 Handle(httpMethod, relativePath string, handlers ...HandlerFunc) IRoutes方法,后面可以传入多个handler来处理具体的业务逻辑,当handler有多个时最后一个处理实际的业务请求,前面的handler来处理中间件和共享的组件

    // 而HandlerFunc 其实就是一个func(*Context)的匿名函数.Context会在下一节具体分析
    ginObj.Any("/hello",func(c *gin.Context){
        // context结构体相关的方法下一节会具体分析,这里是一个简单的示例
        c.String(http.StatusOK,"Hello BGBiao.")
    })


    // 当所有的路由注册之后,我们可以使用gin的结构体方法(engine结构体的引用对象)来实际运行HTTP服务,以接收用户的http请求
    // 我们前面说过该方法除非出现错误,否则会无期限阻塞调用goroutine来接收请求
    ginObj.Run("localhost:8080")
}
#运行实例
$ go run case1.go
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:	export GIN_MODE=release
 - using code:	gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /hello                    --> main.main.func1 (3 handlers)
[GIN-debug] POST   /hello                    --> main.main.func1 (3 handlers)
[GIN-debug] PUT    /hello                    --> main.main.func1 (3 handlers)
[GIN-debug] PATCH  /hello                    --> main.main.func1 (3 handlers)
[GIN-debug] HEAD   /hello                    --> main.main.func1 (3 handlers)
[GIN-debug] OPTIONS /hello                    --> main.main.func1 (3 handlers)
[GIN-debug] DELETE /hello                    --> main.main.func1 (3 handlers)
[GIN-debug] CONNECT /hello                    --> main.main.func1 (3 handlers)
[GIN-debug] TRACE  /hello                    --> main.main.func1 (3 handlers)
[GIN-debug] Listening and serving HTTP on localhost:8080
#模拟请求(因为我们注册了全部的HTTP方法的路由)
$ curl localhost:8080/hello
Hello BGBiao.%                                                                                        
$ curl -X POST  localhost:8080/hello
Hello BGBiao.%                                                                                        
$ curl -X DELETE   localhost:8080/hello
Hello BGBiao.%                                                                                        
$ curl -X TRACE   localhost:8080/hello
Hello BGBiao.%

在上一篇文章深入gin框架内幕(一)中,主要介绍了Gin框架中是如何创建一个HTTP服务以及内部的核心结构和常用的一些结构体方法,并在最后以一个简单的示例来详细讲解Gin框架内部具体是如何运行的,但是在最后我们会发现使用了一个Context引用对象的一些方法来返回具体的HTTP响应数据,在本篇文章中,我们将继续学习和分析Gin框架内幕。

在开始分析之前,我们先简单回顾一下上一个章节中讲到的Gin框架中的几个核心的结构.

Gin框架中的几个核心结构

Gin框架中的几个重要的模型:

  • Engine: 用来初始化一个gin对象实例,在该对象实例中主要包含了一些框架的基础功能,比如日志,中间件设置,路由控制(组),以及handlercontext等相关方法.源码文件
  • Router: 用来定义各种路由规则和条件,并通过HTTP服务将具体的路由注册到一个由context实现的handler中
  • Context: Context是框架中非常重要的一点,它允许我们在中间件间共享变量,管理整个流程,验证请求的json以及提供一个json的响应体. 通常情况下我们的业务逻辑处理也是在整个Context引用对象中进行实现的.
  • Bind: 在Context中我们已经可以获取到请求的详细信息,比如HTTP请求头和请求体,但是我们需要根据不同的HTTP协议参数来获取相应的格式化
    数据来处理底层的业务逻辑,就需要使用Bind相关的结构方法来解析context中的HTTP数据
1.Gin框架对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结构模型。

    2.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结构体常用的一些方法

      基本方法:

      • Copy(): 返回当前正在使用的context的拷贝(context指针),当这个context必须在goroutine中用时,该方法比较有用
      • HandlerName(): 返回当前主handler的名称(比如:handler为handleGetUsers(),该方法将返回"main.handleGetUsers")
      • HandlerNames(): 返回所有注册的handler的名称
      • Handler(): 返回当前的主handler(func (c *Context) Handler() HandlerFunc)
      • FullPath(): 返回一个匹配路由的全路径(uri: "/user/:id",c.FullPath() == "/user/:id" )

      http常用方法:

      • ClientIP() string: 返回客户端ip(该方法会解析X-Real-IP,X-Forwarded-For)
      • ContentType() string: 返回HTTP的Content-Type头
      • IsWebsocket() bool: 返回是否为ws链接

      流控相关的方法:

      • Next(): 该方法仅被使用在middleware中,它会在被调用的handler链内部执行pending handler
      • IsAborted(): 如果当前的context被终止了,该方法返回true
      • Abort(): 该函数可以从正在被调用中保护pending handler. 该方法停止后不会停止当前正在执行的handler. 比如我们有一个鉴权的中间件来验证请求是否有权限,如果认证失败了(用户信息异常等),此时调用Abort()来确保后面的handler不再被调用
      • AbortWithStatus(code int): 同上,在会写入状态码。context.AbortWithStatus(401)即可表示上述的鉴权失败
      • AbortWithStatusJSON(code int, jsonObj interface{}): 同上,会再加响应数据.该方法会停止整个handler链,再写入状态码和json的响应体,同时也会设置Content-Type="application/json"
      • AbortWithError(code int, err error) *Error: 同上返回错误信息

      错误管理:

      • Error(err error) *Error: 返回一些错误对象

      元数据管理:

      • Set(key string, value interface{}): 给当前这个context设置一个新的键值对
      • Get(key string) (value interface{}, exists bool): 返回指定的key的值,以及是否存在
      • MustGet(key string) interface{}: 返回指定key的值,不存在则panic
      • GetString(key string) (s string): 以string类型返回指定的key
      • GetBool(key string) (b bool): 返回分配给该key的值(bool类型)
      • GetInt(key string) (i int):
      • GetStringSlice(key string) (ss []string): 返回key的slice类型
      • GetStringMap(key string) (sm map[string]interface{}): 返回interface{}类型的map结构
      • GetStringMapString(key string) (sms map[string]string): 返回string类型的map结构
      • GetStringMapStringSlice(key string) (smss map[string][]string): 同理

      输入数据:

      • Param(key string) string: 返回URL的参数值(uri_patten: "/user/:id",url: "/user/john",c.Param("id") = "john")
      • Query(key string) string: 返回url中的查询参数值(url: "/path?id=1234&name=Manu&value=",c.Query("id")为1234,c.Query("name")为Manu,c.Query("value")为空)
      • DefaultQuery(key, defaultValue string) string: 返回url中的查询参数的默认值(同上,但是c.Query("value")就没有值,该方法可以设置默认值)
      • GetQuery(key string) (string, bool): 同Query()方法,并且会返回状态,如果对应的key不存在,返回("",false)
      • QueryArray(key string) []string: 返回指定key的对应的array(slice的长度取决于给定key的参数的数量)
      • GetQueryArray(key string) ([]string, bool): 同上,会返回状态
      • QueryMap(key string) map[string]string: 返回指定key对应map类型
      • GetQueryMap(key string) (map[string]string, bool): 同上,并且会返回状态
      • PostForm(key string) string: 该方法返回一个从POST 请求的urlencode表单或者multipart表单数据,不存在时返回空字符串
      • DefaultPostForm(key, defaultValue string) string: 同上,key不存在时返回默认值
      • GetPostForm(key string) (string, bool): 同PostForm()方法,并且会返回状态
      • PostFormArray(key string) []string: 该方法返回指定key的字符串类型的slice
      • GetPostFormArray(key string) ([]string, bool): 同上,并返回状态
      • PostFormMap(key string) map[string]string: 返回指定key的map类型
      • GetPostFormMap(key string) (map[string]string, bool): 同上,并返回状态
      • FormFile(name string) (*multipart.FileHeader, error): 返回指定key的第一个文件(用作文件上传)
      • MultipartForm() (*multipart.Form, error): 该方法解析multipart表单,包含file文件上传
      • SaveUploadedFile(file *multipart.FileHeader, dst string) error: 该方法用来上传指定的文件头到目标路径(dst)

      Bind家族相关方法:

      • Bind(obj interface{}) error: 自动解析Content-Type并绑定到指定的binding引擎
      • BindJSON(obj interface{}) error: 同上,binding引擎为binding.JSON
      • BindXML(obj interface{}) error:
      • BindQuery(obj interface{}) error:
      • BindYAML(obj interface{}) error:
      • BindHeader(obj interface{}) error:
      • BindUri(obj interface{}) error: 使用binding.Uri来绑定传递的结构体指针
      • MustBindWith(obj interface{}, b binding.Binding) error: 使用指定的binding引擎来绑定传递的结构体指针(当有任何错误时,终止请求并返回400)

      ShouldBind家族相关方法:

      • ShouldBind(obj interface{}) error: 同上述的Bind()方法,但是该方法在json结构无效时不会返回400
      • ShouldBindJSON(obj interface{}) error:
      • ShouldBindXML(obj interface{}) error:
      • ShouldBindQuery(obj interface{}) error:
      • ShouldBindYAML(obj interface{}) error:
      • ShouldBindHeader(obj interface{}) error:
      • ShouldBindUri(obj interface{}) error:
      • ShouldBindWith(obj interface{}, b binding.Binding) error: 等同于MustBindWith()方法
      • ShouldBindBodyWith(obj interface{}, bb binding.BindingBody) (err error): 和ShouldBindWith()方法相似,但是他会存储请求体到context中,当下次调用时可以重用(因为该方法是在binding之前读取body,因此在你只使用一次时,为了更好的性能还是使用ShouldBindWith会比较好)

      HTTP响应相关的方法:

      • Status(code int): 设置http的响应码
      • Header(key, value string): 是c.Writer.Header().Set(key, value)的简单实现,在响应体重写入一个header,如果value为空,则相当于调用了c.Writer.Header().Del(key)
      • GetHeader(key string) string: 返回请求体重的header
      • GetRawData() ([]byte, error): 返回流式数据
      • SetCookie(name, value string, maxAge int, path, domain string, secure, httpOnly bool): 该方法将设置一个Set-Cookie到ResponseWriter的头中(注意:name必须是一个合法可用的名称,无效的coookie可能会被丢弃)
      • Cookie(name string) (string, error): 返回名称为name的cookie
      • Render(code int, r render.Render): 该方法写入响应头并调用render.Render去渲染数据
      • HTML(code int, name string, obj interface{}): 该方法使用指定文件模板名称去渲染http模板(同时会更新状态码并设置Content-Type as "text/html".)
      • IndentedJSON(code int, obj interface{}): 该方法会序列化对象obj为一个pretty JSON 数据到响应体中,同时设置Content-Type as "application/json"(pretty JSON需要消耗cpu和带宽,强烈建议生产使用Context.JSON())
      • SecureJSON(code int, obj interface{}): 同上,会序列化成 Secure Json
      • JSONP(code int, obj interface{}):
      • JSON(code int, obj interface{}): 序列化为JSON,并写Content-Type:"application/json"头
      • AsciiJSON(code int, obj interface{}):
      • PureJSON(code int, obj interface{}):
      • XML(code int, obj interface{}): 序列化成xml格式,并写Content-Type:"application/xml"
      • YAML(code int, obj interface{}): 序列化成yaml
      • ProtoBuf(code int, obj interface{}): 序列化成probuf
      • String(code int, format string, values ...interface{}): 将制定的string写入响应体
      • Redirect(code int, location string): 重定向
      • Data(code int, contentType string, data []byte): 写一些数据到响应体重,并更新响应码
      • DataFromReader(code int, contentLength int64, contentType string, reader io.Reader, extraHeaders map[string]string): 写一些制定模板的数据到响应体中,并更新状态码
      • File(filepath string): 以一种高效方式将制定文件写入响应体数据中
      • FileAttachment(filepath, filename string): 同上,但是在客户端文件会被直接下载下来
      • SSEvent(name string, message interface{}): 写Server-Sent Event到响应数据中
      • Stream(step func(w io.Writer) bool) bool: 发送一个流式的响应数据并返回状态
      3.Gin实例示例

      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用户数据输入

          当然到这里后,你可能还会有新的疑问,就是通常情况下,我们开发后端接口会提供一些具体的参数,通过一些具体数据提交来实现具体的业务逻辑处理,这些参数通常会分为如下三类:

          • 使用HTTP GET方法获取到的url中的一些查询参数来执行更具体的业务逻辑(比如我们查询数据的指定条数之类的)
          • 使用HTTP POST GET等其他方式以form表单方式提交的数据来验证和处理用户数据
          • 在URL中获取一些可变参数(比如通常我们的url会定义为"/api/uid/:id"来表示用户id相关的接口,这个时候通常需要获取到url中的id字段)

          以上的基本需求,几乎都可以在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
          }%
          
             
             
            评论
            添加红包

            请填写红包祝福语或标题

            红包个数最小为10个

            红包金额最低5元

            当前余额3.43前往充值 >
            需支付:10.00
            成就一亿技术人!
            领取后你会自动成为博主和红包主的粉丝 规则
            hope_wisdom
            发出的红包
            实付
            使用余额支付
            点击重新获取
            扫码支付
            钱包余额 0

            抵扣说明:

            1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
            2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

            余额充值