第68节 深入学习 gin 源码


❤️💕💕记录sealosopen in new window开源项目的学习过程。k8s,docker和云原生的学习open in new window。Myblog:http://nsddd.topopen in new window


[TOC]

Why

Gin 是一个用 Golang编写的 高性能的web 框架, 由于http路由的优化,速度提高了近 40 倍。 Gin的特点就是封装优雅、API友好。

Gin的一些特性:

  • 快速: 基于 Radix 树的路由,小内存占用。没有反射。可预测的 API 性能。
  • 支持中间件:入的 HTTP 请求可以由一系列中间件和最终操作来处理。 例如:Logger,Authorization,GZIP,最终操作 DB。
  • Crash 处理:Gin 可以 catch 一个发生在 HTTP 请求中的 panic 并 recover 它。这样,你的服务器将始终可用。例如,你可以向 Sentry 报告这个 panic!
  • JSON 验证:Gin 可以解析并验证请求的 JSON,例如检查所需值的存在。
  • 路由组:更好地组织路由。是否需要授权,不同的 API 版本…… 此外,这些组可以无限制地嵌套而不会降低性能。
  • 错误管理:Gin 提供了一种方便的方法来收集 HTTP 请求期间发生的所有错误。最终,中间件可以将它们写入日志文件,数据库并通过网络发送。
  • 内置渲染:Gin 为 JSON,XML 和 HTML 渲染提供了易于使用的 API。
  • 可扩展性:新建一个中间件非常简单。

下载并安装 gin

go get -u github.com/gin-gonic/gin

项目结构

实际项目业务功能和模块会很多,我们不可能把所有代码都写在一个go文件里面或者写在一个main入口函数里面;我们需要对项目结构做一些规划,方便维护代码以及扩展。

Gin框没有对项目结构做出限制,我们可以根据自己项目需要自行设计。

MVC 架构设计:

├── conf            #项目配置文件目录
│   └── config.toml #大家可以选择自己熟悉的配置文件管理工具包例如:toml、xml等等
├── controllers     #控制器目录,按模块存放控制器(或者叫控制器函数),必要的时候可以继续划分子目录。
│   ├── food.go
│   └── user.go
├── main.go         #项目入口,这里负责Gin框架的初始化,注册路由信息,关联控制器函数等。
├── models          #模型目录,负责项目的数据存储部分,例如各个模块的Mysql表的读写模型。
│   ├── food.go
│   └── user.go 
├── static          #静态资源目录,包括Js,css,jpg等等,可以通过Gin框架配置,直接让用户访问。
│   ├── css
│   ├── images
│   └── js
├── logs            #日志文件目录,主要保存项目运行过程中产生的日志。
└── views           #视图模板目录,存放各个模块的视图模板,当然有些项目只有api,是不需要视图部分,可以忽略这个目录
    └── index.html

Gin 框架运行模式

为方便调试,Gin 框架在运行的时候默认是debug模式,在控制台默认会打印出很多调试日志,上线的时候我们需要关闭debug模式,改为release模式。

export GIN_MODE=release

GIN_MODE 环境变量,可以设置为debug或者release

通过代码设置

在main函数,初始化gin框架的时候执行下面代码
// 设置 release模式
gin.SetMode(gin.ReleaseMode)
// 或者 设置debug模式
gin.SetMode(gin.DebugMode)

Gin 路由和控制器

路由是一个过程,指的是一个http请求,如何找到对应的处理器函数(也可以叫控制器函数), Gin框架的路由是基于httprouter包实现的。

控制器函数主要负责执行 http请求-响应任务

💡简单的一个案例如下:

r := gin.Default()

// 路由定义post请求, url路径为:/user/login, 绑定doLogin控制器函数
r.POST("/user/login", doLogin)

// 控制器函数
func doLogin(c *gin.Context) {
        // 获取post请求参数
	username := c.PostForm("username")
	password := c.PostForm("password")

	// 通过请求上下文对象Context, 直接往客户端返回一个字符串
	c.String(200, "username=%s,password=%s", username,password)
}

路由规则

一条路由规则由三部分组成:

  • http请求方法
  • url路径
  • 控制器函数

http请求方法:

常用的http请求方法有下面4种:

  • GET
  • POST
  • PUT
  • DELETE

url路径:

echo框架,url路径有三种写法:

  • 静态url路径
  • 带路径参数的url路径
  • 带星号(*)模糊匹配参数的url路径

💡简单的一个案例如下:

/ 例子1, 静态Url路径, 即不带任何参数的url路径
/users/center
/user/111
/food/12

// 例子2,带路径参数的url路径,url路径上面带有参数,参数由冒号(:)跟着一个字符串定义。
// 路径参数值可以是数值,也可以是字符串

//定义参数:id, 可以匹配/user/1, /user/899 /user/xiaoli 这类Url路径
/user/:id

//定义参数:id, 可以匹配/food/2, /food/100 /food/apple 这类Url路径
/food/:id

//定义参数:type和:page, 可以匹配/foods/2/1, /food/100/25 /food/apple/30 这类Url路径
/foods/:type/:page

// 例子3. 带星号(*)模糊匹配参数的url路径
// 星号代表匹配任意路径的意思, 必须在*号后面指定一个参数名,后面可以通过这个参数获取*号匹配的内容。

//以/foods/ 开头的所有路径都匹配
//匹配:/foods/1, /foods/200, /foods/1/20, /foods/apple/1 
/foods/*path

//可以通过path参数获取*号匹配的内容。

控制器函数:

控制器函数定义:

func HandlerFunc(c *gin.Context)

控制器函数接受一个上下文参数。

可以通过上下文参数,获取http请求参数,响应http请求。

💡简单的一个案例如下:

//实例化gin实例对象。
r := gin.Default()
	
//定义post请求, url路径为:/users, 绑定saveUser控制器函数
r.POST("/users", saveUser)

//定义get请求,url路径为:/users/:id  (:id是参数,例如: /users/10, 会匹配这个url模式),绑定getUser控制器函数
r.GET("/users/:id", getUser)

//定义put请求
r.PUT("/users/:id", updateUser)

//定义delete请求
r.DELETE("/users/:id", deleteUser)


//控制器函数实现
func saveUser(c *gin.Context) {
    ...忽略实现...
}

func getUser(c *gin.Context) {
    ...忽略实现...
}

func updateUser(c *gin.Context) {
    ...忽略实现...
}

func deleteUser(c *gin.Context) {
    ...忽略实现...
}

分组路由

在我们做 API 的时候,如果要支持多个 API 的版本,我们可以通过 分组路由来实现

💡简单的一个案例如下:

func main() {
	router := gin.Default()

	// 创建v1组
	v1 := router.Group("/v1")
	{
                // 在v1这个分组下,注册路由
		v1.POST("/login", loginEndpoint)
		v1.POST("/submit", submitEndpoint)
		v1.POST("/read", readEndpoint)
	}

	// 创建v2组
	v2 := router.Group("/v2")
	{
                // 在v2这个分组下,注册路由
		v2.POST("/login", loginEndpoint)
		v2.POST("/submit", submitEndpoint)
		v2.POST("/read", readEndpoint)
	}

	router.Run(":8080")
}

上面的例子将会注册下面的路由信息:

  • /v1/login
  • /v1/submit
  • /v1/read
  • /v2/login
  • /v2/submit
  • /v2/read

Note

一般还是推荐采用合适的目录结构进行分组

Gin如何处理请求参数

Get 请求

Get请求url例子:*/path?id=1234&name=Manu&value=*111

# 获取Get请求参数的常用函数:
func (c *Context) Query(key string) string
func (c *Context) DefaultQuery(key, defaultValue string) string
func (c *Context) GetQuery(key string) (string, bool)

💡简单的一个案例如下:

func Handler(c *gin.Context) {
	//获取name参数, 通过Query获取的参数值是String类型。
	name := c.Query("name")

        //获取name参数, 跟Query函数的区别是,可以通过第二个参数设置默认值。
        name := c.DefaultQuery("name", "tizi365")

	//获取id参数, 通过GetQuery获取的参数值也是String类型, 
	// 区别是GetQuery返回两个参数,第一个是参数值,第二个参数是参数是否存在的bool值,可以用来判断参数是否存在。
	id, ok := c.GetQuery("id")
        if !ok {
	   // 参数不存在
	}
}

Post 请求

常用的函数:

func (c *Context) PostForm(key string) string
func (c *Context) DefaultPostForm(key, defaultValue string) string
func (c *Context) GetPostForm(key string) (string, bool)

💡简单的一个案例如下:

func Handler(c *gin.Context) {
	//获取name参数, 通过PostForm获取的参数值是String类型。
	name := c.PostForm("name")

	// 跟PostForm的区别是可以通过第二个参数设置参数默认值
	name := c.DefaultPostForm("name", "tizi365")

	//获取id参数, 通过GetPostForm获取的参数值也是String类型,
	// 区别是GetPostForm返回两个参数,第一个是参数值,第二个参数是参数是否存在的bool值,可以用来判断参数是否存在。
	id, ok := c.GetPostForm("id")
	if !ok {
	    // 参数不存在
	}
}

获取URL路径参数

获取URL路径参数,指的是获取 /user/:id这类型路由绑定的参数,这个例子绑定了一个参数id。

获取url路径参数常用函数:

func (c *Context) Param(key string) string

💡简单的一个案例如下:

r := gin.Default()
	
r.GET("/user/:id", func(c *gin.Context) {
	// 获取url参数id
	id := c.Param("id")
})

将请求参数绑定到struct对象

前面获取参数的方式都是一个个参数的读取,比较麻烦,Gin框架支持将请求参数自动绑定到一个struct对象,这种方式支持Get/Post请求,也支持http请求body内容为json/xml格式的参数。

下面例子是将请求参数绑定到User struct对象。

// User 结构体定义
type User struct {
  Name  string `json:"name" form:"name"`
  Email string `json:"email" form:"email"`
}

通过定义 struct 字段的标签,定义请求参数和 struct 字段的关系。 下面对 UserName 字段的标签进行说明。

struct标签说明:

标签说明
json:"name"数据格式为json格式,并且json字段名为name
form:"name"表单参数名为name

下面是控制器代码:

r.POST("/user/:id", func(c *gin.Context) {
   // 初始化user struct
   u := User{}
   // 通过ShouldBind函数,将请求参数绑定到struct对象, 处理json请求代码是一样的。
   // 如果是post请求则根据Content-Type判断,接收的是json数据,还是普通的http请求参数
   if c.ShouldBind(&u) == nil {
     // 绑定成功, 打印请求参数
     log.Println(u.Name)
     log.Println(u.Email)

    }
    // http 请求返回一个字符串 
    c.String(200, "Success")
})

Gin如何获取客户ip

r := gin.Default()
	
r.GET("/ip", func(c *gin.Context) {
	// 获取用户IP
	ip := c.ClientIP()
})

Gin处理请求结果

gin.Context上下文对象支持多种返回处理结果,下面分别介绍不同的响应方式。

以字符串方式响应请求

通过String函数返回字符串。

函数定义:

func (c *Context) String(code int, format string, values ...interface{})

参数说明:

参数说明
codehttp状态码
format返回结果,支持类似Sprintf函数一样的字符串格式定义,例如,%d 代表插入整数,%s代表插入字符串
values任意个format参数定义的字符串格式参数

💡简单的一个案例如下:

func Handler(c *gin.Context)  {
	// 例子1:
	c.String(200, "欢迎访问nsddd.top!")
	
	// 例子2: 这里定义了两个字符串参数(两个%s),后面传入的两个字符串参数将会替换对应的%s
	c.String(200,"欢迎访问%s, 你是%s", "nsddd.top!","最靓的仔!")
}

以json格式响应请求

我们开发api接口的时候常用的格式就是json,下面是返回json格式数据的例子

// User 定义
type User struct {
  Name  string `json:"name"` // 通过json标签定义struct字段转换成json字段的名字。
  Email string `json:"email"`
}

// Handler 控制器
func(c *gin.Context) {
  //初始化user对象
  u := &User{
    Name:  "Xinwei Xiong",
    Email: "3293172751nss@gmail.com",
  }
  //返回json数据
  //返回结果:{"name":"Xinwei Xiong", "email":"3293172751nss@gmail.com"}
  c.JSON(200, u)
}

以xml格式响应请求

type User struct {
  Name  string `xml:"name"` // 通过xml标签定义struct字段转换成xml字段的名字。
  Email string `xml:"email"`
}

c.XML(200, u)

以文件格式响应请求

下面介绍gin框架如何直接返回一个文件,可以用来做文件下载。

func(c *gin.Context) {
  //通过File函数,直接返回本地文件,参数为本地文件地址。
  //函数说明:c.File("文件路径")
  c.File("/var/www/1.jpg")
}

例子2func(c *gin.Context) {
  //通过FileAttachment函数,返回本地文件,类似File函数,区别是可以指定下载的文件名。
  //函数说明: c.FileAttachment("文件路径", "下载的文件名")
  c.FileAttachment("/var/www/1.jpg", "1.jpg")
}

html模板处理

Gin 框架默认封装了golang内置的 html/template 包用于处理html模版。

使用到了 Go语言 的 template 模板引擎的特性:

Go语言模板引擎

Go语言内置了 text/template 和 html/template两个模板库,专门用于处理网页html模板。 html/template 是在 text/template 模板库的基础上增加了对html输出的安全处理,主要目的是为了防止被攻击。

模版引擎使用流程:

  1. 编写模版代码
  2. 导入包
  3. 加载模版代码
  4. 根据模版参数渲染模版

💡简单的一个案例如下:

将下面模版代码保存至views/demo.tpl文件中, 文件后缀名随意。

{{define "demo"}}
昵称: {{.Name}},
{{- if .IsWin}}
恭喜,大吉大利,今晚吃鸡!
{{- else}}
遗憾,鸡被吃光了!
{{- end}}
{{- end}}

define "模板名" 用于定义子模板,后面渲染模板会用到这个名字。

然后我们导入包

import "text/template"

加载模版代码:

// 加载模版代码,并且创建template对象t
// template.ParseGlob 函数加载views目录下的所有tpl为后缀的模版文件
// template.Must函数主要用于检测加载的模版有没有错误,有错误输出panic错误,并且结束程序。
t := template.Must(template.ParseGlob("./views/*.tpl"))

根据模版参数渲染模版

定义模版参数:

//这里定义一个struct类型的模版参数,实际上模版可以是任意类型数据
type GameStatus struct {
    Name string
    IsWin bool
}

因为只能传入一个模版参数,如果想传入多个模版参数,可以使用map或者struct类型。

初始化模板参数, 这里初始化一个参数数组,下面用于循环渲染模板。

var userStatus = []GameStatus{
    {"大春", true},
    {"NiuBee", false},
    {"球球", true},
}

下面是根据userStatus 数组循环渲染模板

for _, u := range userStatus {
    //根据参数u, 渲染命名为demo的模板,并且将渲染结果打印到标准输出
    err := t.ExecuteTemplate(os.Stdout, "demo", u)
    if err != nil {
        log.Println("executing template:", err)
    }
}

输出结果:

昵称: 大春,
恭喜,大吉大利,今晚吃鸡!
昵称: NiuBee,
遗憾,鸡被吃光了!
昵称: 球球,
恭喜,大吉大利,今晚吃鸡!

Gin 返回模板的案例

💡简单的一个案例如下:

func main() {
        // 初始化gin对象
	router := gin.Default()
        // 首先加载templates目录下面的所有模版文件,模版文件扩展名随意
	router.LoadHTMLGlob("templates/*")

        // 绑定一个url路由 /index
	router.GET("/index", func(c *gin.Context) {
                // 通过HTML函数返回html代码
                // 第二个参数是模版文件名字
                // 第三个参数是map类型,代表模版参数
                // gin.H 是map[string]interface{}类型的别名
		c.HTML(http.StatusOK, "index.html", gin.H{
			"title": "Main website",
		})
	})
        // 启动http服务,并且绑定在8080端口
	router.Run(":8080")
}

模版代码

文件名:templates/index.html

import (
    "fmt"

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

func main() {

    router := gin.Default()

    router.GET("/cookie", func(c *gin.Context) {
       // 设置cookie
       c.SetCookie("site_cookie", "cookievalue", 3600, "/", "localhost", false, true)
    })

    router.Run()/h1>
</html>

Gin框架如何处理cookie

cookie通常用于在浏览器中保存一些小数据,例如客户标识、用户非铭感数据。注意别使用cookie保存隐私数据。

gin框架主要通过上下文对象提供的SetCookie和Cookie两个函数操作cookie

设置cookie

import (
    "fmt"

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

func main() {

    router := gin.Default()

    router.GET("/cookie", func(c *gin.Context) {
       // 设置cookie
       c.SetCookie("site_cookie", "cookievalue", 3600, "/", "localhost", false, true)
    })

    router.Run()
}

SetCookie函数定义:

func (c *Context) SetCookie(name, value string, maxAge int, path, domain string, secure, httpOnly bool)

参数说明:

参数名类型说明
namestringcookie名字
valuestringcookie值
maxAgeint有效时间,单位是秒,MaxAge=0 忽略MaxAge属性,MaxAge<0 相当于删除cookie, 通常可以设置-1代表删除,MaxAge>0 多少秒后cookie失效
pathstringcookie路径
domainstringcookie作用域
secureboolSecure=true,那么这个cookie只能用https协议发送给服务器
httpOnlybool设置HttpOnly=true的cookie不能被js获取到
func Handler(c *gin.Context) {
      // 根据cookie名字读取cookie值
      data, err := c.Cookie("site_cookie")
      if err != nil {
	 // 直接返回cookie值
	 c.String(200,data)
	 return
      }
      c.String(200,"not found!")
}

通过将cookie的MaxAge设置为-1, 达到删除cookie的目的。

func Handler(c *gin.Context) {
      // 设置cookie  MaxAge设置为-1,表示删除cookie
       c.SetCookie("site_cookie", "cookievalue", -1, "/", "localhost", false, true)
      c.String(200,"删除cookie演示")
}

Go 和 Gin 开发 RESTful API

Gin 源码学习

Gin 的源码主要由以下几部分组成:

  1. Engine:代表一个 Gin 实例,维护中间件、路由信息等

  2. Router:实现路由查找及分发请求的逻辑

  3. Context:封装 Request 和 Response,提供一系列方便的方法访问 request/response

  4. Middleware: 中间件相关代码

  5. Render: 提供 JSON、XML、YAML、HTML 等响应渲染方法

  6. File: 文件上传及服务相关方法

  7. ErrorHandler: 错误处理中间件

这些部分组合在一起,构成了 Gin 这个轻量级 Web 框架。

END 链接