返回

Gin Introduction

Go第三方库之web框架gin。


Gin介绍

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

Gin 最擅长的就是API接口的高并发,如果项目的规模不大,业务相对简单,这个时候我们也推荐您使用 Gin。

当某个接口的性能遭到较大挑战的时候,这个还是可以考虑使用 Gin 重写接口。

Gin 也是一个流行的 golang Web 框架,Github Strat 量已经超过了 75k[2024/05/21]。

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 的官网: https://gin-gonic.com/zh-cn/

Gin Github 地址: https://github.com/gin-gonic/gin


快速使用

下载并安装 gin:

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

注意:go版本要 1.6 及以上。

快速使用示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

// 导入gin包
import "github.com/gin-gonic/gin"

// 入口函数
func main() {
    // 初始化一个http服务对象,创建一个默认的路由引擎对象
    r := gin.Default()

    // 配置路由
    // 设置一个get请求的路由,url为/ping, 处理函数(或者叫控制器函数、回调函数)是一个闭包函数。
    r.GET("/ping", func(c *gin.Context) {
       // 通过请求上下文对象Context, 直接往客户端返回一个json
       c.JSON(200, gin.H{
          "message": "pong",
       })
    })

    err := r.Run() // 启动 HTTP 服务,默认在 0.0.0.0:8080 启动服务
    if err != nil {
       panic("Http serve start error:" + err.Error())
    }
}

运行命令go run main.go,即可启动http服务。然后就可以通过localhost:8080/ping 访问了。会返回如下内容:

1
2
3
{
"message": "pong"
}

注意:如果不期望在测试的时候,每次都弹出防火墙警告,可将监听地址、端口改为localhost:8080。生产环境应该为:port,省略地址,默认为0.0.0.0

Gin框架热重载

所谓热重载就是当我们对代码进行修改时,程序能够自动重新加载并执行,这在我们开发中是非常便利的,可以快速进行代码测试,省去了每次手动重新编译。

Gin框架并没有提供热重载的功能,这个时候我们要实现热重载就要借助第三方的工具。

这里推荐一个使用最多的gin框架热重载工具:air。它是在 fresh 的基础上诞生的。

特性:

  • 彩色的日志输出
  • 自定义构建或必要的命令
  • 支持外部子目录
  • 在 Air 启动之后,允许监听新创建的路径
  • 更棒的构建过程

原理大致是:监听到文件系统修改通知后,重新构建应用程序。

air基本使用

安装:

1
go install github.com/cosmtrek/air@latest

注意:go版本要1.22 或更高。

安装成功之后,就可以在gin项目根目录下直接运行air命令,就可以启动gin项目,实现热重载。

air默认情况下使用默认配置启动服务。如果要修改默认配置,可以运行air init生成默认的配置文件.air.toml,修改之后,运行air即可使用修改的配置启动服务,注意:如果修改了配置文件名,需要用-c选项指定新的配置文件。否则将使用默认的.air.toml启动服务。

一般使用默认配置就够用了。可修改的内容如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"

[build]
  args_bin = []
  bin = "tmp\\main.exe"
  cmd = "go build -o ./tmp/main.exe ."
  delay = 1000
  exclude_dir = ["assets", "tmp", "vendor", "testdata"]
  exclude_file = []
  exclude_regex = ["_test.go"]
  exclude_unchanged = false
  follow_symlink = false
  full_bin = ""
  include_dir = []
  include_ext = ["go", "tpl", "tmpl", "html"]
  include_file = []
  kill_delay = "0s"
  log = "build-errors.log"
  poll = false
  poll_interval = 0
  post_cmd = []
  pre_cmd = []
  rerun = false
  rerun_delay = 500
  send_interrupt = false
  stop_on_error = false

[color]
  app = ""
  build = "yellow"
  main = "magenta"
  runner = "green"
  watcher = "cyan"

[log]
  main_only = false
  time = false

[misc]
  clean_on_exit = false

[proxy]
  app_port = 0
  enabled = false
  proxy_port = 0

[screen]
  clear_on_rebuild = false
  keep_scroll = true

基本使用如上,更多内容参考air官方

项目结构

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

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

这里给出一个典型的MVC框架大致的项目结构的例子,大家可以参考下:

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

Gin框架运行模式

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

设置Gin框架运行模式:

  1. 通过环境变量设置GIN_MODE,如:

    1
    
    export GIN_MODE=release
    

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

  2. 通过代码设置:

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

路由与控制器

Gin框架中的路由是指通过HTTP请求的路径找到对应的控制器函数(也可以叫处理器函数)。Gin框架的路由是基于httprouter包实现的。

控制器函数主要负责处理http请求和响应请求。

一个简单的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
r := gin.Default() // 创建默认的路由引擎对象

// 配置路由:post请求, uri路径为:/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:查Read-select
  • POST:增Create-insert
  • PUT:改Update-update
  • DELETE: 删Delete-delete

目前常用的API风格是RESTful风格。

在RESTful风格中,每个路径表示不同的资源,通过不同的请求方式访问相同的路径表示执行不同的操作(CRUD)。

RESTful CRUD示例:

1
2
3
4
5
POST /users      # 创建一个新用户
GET /users/1     # 获取ID为1的用户
PUT /users/1     # 更新ID为1的用户
DELETE /users/1  # 删除ID为1的用户
				 # 注意:1是路径参数

url路径

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

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

下面看下各种url路由的例子

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 例子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参数获取*号匹配的内容。

控制器函数

控制器函数定义:

1
type HandlerFunc func(*Context)

控制器函数接受一个上下文参数。 可以通过上下文参数,获取http请求参数,响应http请求。

配置路由示例

配置路由就是利用路由规则定义路由:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//实例化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) {
    ...忽略实现...
}

提示:实际项目开发中不要把路由定义和控制器函数都写在一个go文件,不方便维护,可以参考第一章的项目结构,规划自己的业务模块。

路由分组

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

示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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

路由分组,其实就是设置了同一类路由的url前缀

利用路由分组,我们可以实现将需要授权和不需要授权的API进行分组管理(后面介绍)。同时也能够将他们分文件保存

示例如下:

routers/router.go:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package routers

import (
	"log"

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

// registerRouterGroupFunc 定义类型别名,用于注册路由组的函数
type registerRouterGroupFunc = func(r *gin.Engine)

// 存储所有需要注册的路由组函数
var routerGroups []registerRouterGroupFunc

// 添加路由组函数到路由组列表中
func addRouterGroup(r registerRouterGroupFunc) {
	if r == nil {
		return
	}
	routerGroups = append(routerGroups, r)
}

// 注册所有路由组到主路由组中
func registerRoutes(r *gin.Engine) {
	for _, register := range routerGroups {
		register(r)
	}
}

// 加载所有路由组
func loadRouterGroups() {
	loadDefaultRouter()
	loadManageRouter()
}

// InitRouter 初始化主路由器
func InitRouter() {
	// 创建默认的 Gin 路由器
	r := gin.Default()

	// 加载并注册所有路由组
	loadRouterGroups()
	registerRoutes(r)

	// 启动服务器
	err := r.Run("localhost:8080")
	if err != nil {
		log.Panic("Start http serve error:", err)
	}
}

routers/defaultRouter.go:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package routers

import (
    "github.com/arlettebrook/learn/controllers"
    "github.com/gin-gonic/gin"
)

// 加载所有默认的路由组
// 这里控制器函数没有分组保存
func loadDefaultRouter() {
	addRouterGroup(func(r *gin.Engine) {
        // defaultRouter := r.group("/") 可以省略
        // defaultRouter.Get(...) 与下面等效
        r.GET("/", func(c *gin.Context) {
			c.JSON(http.StatusOK, gin.H{
				"msg": "Hello, world!",
			})
		})
	})
}

routers/manageRouter.go:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package routers

import (
    "github.com/arlettebrook/learn/controllers"
    "github.com/gin-gonic/gin"
)

// 加载所有管理路由组
func loadManageRouter() {
	addRouterGroup(func(r *gin.Engine) {
		manageRouter := r.Group("/manage")
		{
			manageRouter.GET("/addUser", func(c *gin.Context) {
				c.JSON(http.StatusOK, gin.H{
					"msg": "Add user",
				})
			})
		}
	})
}

在main.go中调用routers.InitRouter()即可启动gin框架。浏览器中访问http://localhost:8080/manage/addUserhttp://localhost:8080/就可以访问注册的两条路由。

注意:默认路由组,在/,意思就是:r.Group("/")r.Get("/",...)属于同一组。

控制器函数分组

当我们的项目比较大的时候有必要对我们的控制器进行分组。

参考路由分组示例,使用控制器函数分组可修改为:

routers/defaultRouter.go:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package routers

import (
    "github.com/arlettebrook/learn/controllers"

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

func loadDefaultRouter() {
    addRouterGroup(func(r *gin.Engine) {
       defaultController := controllers.DefaultController{}

       r.GET("/", defaultController.Index)
    })
}

routers/manageRouter.go:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package routers

import (
    "github.com/arlettebrook/learn/controllers"
    "github.com/gin-gonic/gin"
)

func loadManageRouter() {
    addRouterGroup(func(r *gin.Engine) {
       manageRouter := r.Group("/manage")
       manageController := controllers.ManageController{}
       {
          manageRouter.GET("/addUser", manageController.AddUser)
       }
    })
}

添加的控制器分组:

controllers/defaultController.go:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package controllers

import (
    "net/http"

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

type DefaultController struct {
}

func (d DefaultController) Index(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{
       "msg": "Hello, world!",
    })
}

controllers/manageController.go:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package controllers

import (
    "net/http"

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

type ManageController struct {
}

func (m ManageController) AddUser(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{
       "msg": "Add user",
    })
}

注意:为什么要在控制器里面定义对应的结构体对象呢?

  • 防止在同一个包下出现同名方法。
  • 更加语义化,方便调用。
  • 可以实现控制器继承

控制器继承

控制器继承,可以将一些共用的控制器函数封装出来,实现代码优化。

示例:

controllers/baseController.go:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
package controllers

import (
	"net/http"

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

type BaseController struct {
}

func (b *BaseController) Ok(ctx *gin.Context) {
	ctx.JSON(http.StatusOK, gin.H{
		"msg": "Success!",
	})
}

func (b *BaseController) Fail(ctx *gin.Context) {
	ctx.JSON(http.StatusOK, gin.H{
		"msg": "Fail!",
	})
}

controllers/defaultController.go:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package controllers

import (
    "net/http"

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

type DefaultController struct {
    BaseController
}

func (d DefaultController) Index(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{
       "msg": "Hello, world!",
    })
}

routers/defaultRouter.go:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package routers

import (
    "github.com/arlettebrook/learn/controllers"

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

func loadDefaultRouter() {
    addRouterGroup(func(r *gin.Engine) {
       defaultController := controllers.DefaultController{}

       r.GET("/", defaultController.Index)

       r.GET("/extend/s", defaultController.Ok)
       r.GET("/extend/f", defaultController.Fail)
    })
}

所以,我们只需要对应的控制器继承BaseController,那么该控制器就有对应的公有控制器函数。

TODO: 利用控制器,还能实现更多的操作,未完待续…

补充:上面的示例中DefaultController 是无状态的(stateless),即它不在成员变量中保存与特定请求相关的数据,那么是线程安全的,不会发生数据冲突。然而,如果 DefaultController 有状态(stateful),即它在成员变量中保存与请求相关的数据,那么多个用户请求同时访问时可能会发生数据冲突。

以下是一些确保线程安全的方法:

  1. 无状态控制器: 确保控制器不保存任何与请求相关的状态信息。所有的状态信息应保存在请求的上下文中或传递给方法中的局部变量。

    • 使用中间件或局部变量: 如果需要保存请求相关的数据,将这些数据保存在请求的上下文中或方法的局部变量中,而不是控制器的成员变量中。
  2. 每次请求创建新的控制器实例: 在路由器中为每个请求创建一个新的控制器实例。这样每个请求都有自己的控制器实例,不会发生数据冲突。

    1
    2
    3
    4
    
    r.GET("/", func(ctx *gin.Context) {
                defaultController := controllers.DefaultController{}
                defaultController.Index(ctx)
            })
    

获取请求参数

本章介绍Gin框架获取请求参数的方式。

获取查询参数

查询(query)参数是url路径中?后面的所有键值对,他们用&符合连接。url例子:/query?id=1234&name=Manu&value=111

Gin框架获取查询参数的常用函数

  • func (c *Context) Query(key string) string
    • 键不存在返回空字符串。
  • func (c *Context) DefaultQuery(key, defaultValue string) string
    • 键不存在,使用默认值。
  • func (c *Context) GetQuery(key string) (string, bool)
    • 键存在返回值和true。键不存在返回空字符串和false。键的值为空字符串也返回空字符串和false。

注意:

  • 获取的查询参数类型都为string。

  • 通常情况下查询参数出现在GET请求当中,因为GET请求参数会出现在url路径中。当然在POST请求中也能够获取查询参数。

示例:/user?uid=20&page=1

1
2
3
4
5
router.GET("/user", func(c *gin.Context) { 
    uid := c.Query("uid") 
    page := c.DefaultQuery("page", "0") 
    c.String(200, "uid=%v page=%v", uid, page) 
})

获取路径参数

路径(param)参数是包含在url路径中的参数,通常在url中不容易区分,只有在绑定路径参数的地方才能正确判断。

绑定路径参数的语法是/:+参数名,如/user/:id,就是在user路径下绑定一个id参数,那么/user/1, /user/2…, user后面的一级都是路径参数。

当然路径参数也支持嵌套,如:/user/:id/:name/:age, /user/001/jack/18

获取路径参数常用函数:

  • func (c *Context) Param(key string) string
    • 获取路径中指定位置的参数,键不存在,返回空字符串。
    • 注意:键名要与绑定的路径参数名一直,没有冒号。位置要一一对应,否则请求会404.
    • 通常获取GET请求中的路径参数,但也能获取POST请求中的路径参数。
  • 获取路径参数只有这一个方法。

示例:域名/user/20

1
2
3
4
r.POST("/user/:uid", func(c *gin.Context) { 
    uid := c.Param("uid")
    c.String(200, "userID=%s", uid) 
})

获取表单参数

表单参数是位于请求体中的,所以只适用于POST请求。

所以一般说获取表单参数就是获取POST请求参数。

获取Post请求参数的常用函数:

  • func (c *Context) PostForm(key string) string
    • 从请求体(表单)中获取指定键的值,不存在返回空字符串。
  • func (c *Context) DefaultPostForm(key, defaultValue string) string
    • 键不存在,指定默认值。
  • func (c *Context) GetPostForm(key string) (string, bool)
    • 键不存在返回空字符串和false,存在返回值和true。
  • 注意:
    • 获取的参数类型但是string。
    • 以上方法不能获取请求体中的json、xml等原始数据。只能获取表单数据。

示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
r.POST("/doAddUser", func(c *gin.Context) { 
    username := c.PostForm("username") 
    password := c.PostForm("password")
    age := c.DefaultPostForm("age", "20")
    
	c.JSON(200, gin.H{ 
        "usernmae": username, 
        "password": password, 
        "age": age, 
    }) 
})

将请求参数绑定到结构体对象

前面获取参数的方式都是一个个参数的读取,比较麻烦,Gin框架支持根据请求参数类型自动绑定到一个struct对象。

用到的方法是BindShouldBind

  • 二者会根据请求参数的类型自动选择对应的绑定引擎进行绑定

    • 根据请求头的MIME类型判断。
  • 支持查询、表单、请求体原始数据(常用:xml、json)参数,不支持路径参数

    • 如果需要将路径参数绑定到结构体,需要指定绑定引擎为XxxUri,并且用uri标签指定字段。
  • 也可指定对应的绑定引擎就行绑定,如:

    • BindQueryShouldBindQuery

    • BindJSONShouldBindJSON

      1
      2
      
      	c.ShouldBindBodyWithJSON()
      	c.ShouldBindJSON()
      

      二者都是将请求体中的json数据绑定到结构体对象

      区别是:使用ShouldBindBodyWithJSON绑定后,请求体内容不会被消耗,请求体的内容还能用ShouldBindBodyWithJSON重复使用,而其他的绑定方法只能绑定一次会消耗请求体内容,读取后请求体内容不可再用

      如果请求体只读取一次,使用ShouldBindJSON性能更好。读取多次只能用ShouldBindBodyWithJSON。如在中间件中进行验证后再处理业务逻辑。

      一句话带Body的可以使用相同方法重复绑定没有的只能绑定一次。再次绑定会报EOF错误

    • BindUriShouldBindUri

      • 绑定路径参数,注意需要用uri标签指定字段。
  • 绑定的结构体字段需要与参数的键一直,才能绑定成功。如果不一致需要用结构体标签指明。格式为参数类型:键名,常用的绑定标签:

    • form: 指定表单参数查询参数的键。
    • json:指定json参数的键。
    • xml:指定xml参数的键。
    • uri:指定路径参数的键。
    • binding:指定绑定验证,如果验证失败,绑定会失败。多个属性用,分隔。常用属性:
      • 必填字段(required):确保字段在请求中必须存在且不能为空。
      • 最小长度(min): 确保字符串或数组的长度不小于指定值。
      • 最大长度(max): 确保字符串或数组的长度不大于指定值。
      • 正则表达式(regexp): 确保字符串符合指定的正则表达式。
    1
    2
    3
    4
    
    type User struct {
        Id   int    `form:"id" json:"id" xml:"id" uri:"id" binding:"required"`
        Name string `form:"name" json:"name" xml:"name" uri:"name"`
    }
    

    Id字段在表单和查询参数中的键是id,json数据中的键是id,xml数据中的键是id,路径参数中的键是id,并且该字段在请求参数中必须存在且不为空,才能绑定成功。Name字段同理。

    • binding 标签通常与其他标签(如 jsonform 等)结合使用,以指定键名和绑定验证。
  • 二者区别:

    • Bind绑定结构体失败,会自动响应400错误:Bad Request
    • ShouldBind绑定失败不会自动响应错误,需要手动处理响应错误。
    • 一般推荐使用ShouldBind,更容易定制化。

示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

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

type User struct {
    Username string `json:"username" binding:"required,min=3,max=20"`
    Password string `json:"password" binding:"required,min=8"`
    Email    string `json:"email" binding:"required,email"`
    Age      int    `json:"age" binding:"required,min=18,max=65"`
}

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

    r.POST("/register", func(c *gin.Context) {
        var user User
        if err := c.ShouldBindJSON(&user); err != nil {
            c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
            return
        }

        c.JSON(http.StatusOK, gin.H{"status": "registration successful"})
    })

    r.Run(":8080")
}

在这个示例中:

  • Username 字段必须存在且长度在 3 到 20 之间。
  • Password 字段必须存在且长度至少为 8。
  • Email 字段必须存在且符合 email 格式。
  • Age 字段必须存在且值在 18 到 65 之间。

Gin如何获取客户ip

1
2
3
4
5
r.GET("/ip", func(c *gin.Context) {
	// 获取用户IP
	ip := c.ClientIP()
    remoteIP := c.RemoteIP()
})
  • ClientIP方法会尽力返回客户端真实ip。会考虑代理的情况。

  • RemoteIP方法返回与服务器直接连接的ip,不考虑代理的情况。

区别(了解):

  1. 数据来源
    • RemoteIP() 直接从 c.Request.RemoteAddr 获取 IP 地址。
    • ClientIP() 优先从请求头(如 X-Forwarded-ForX-Real-IP 等)中获取 IP 地址,如果请求头中没有这些字段,则退回到 RemoteAddr
  2. 使用场景
    • RemoteIP() 更适用于需要获取直接连接到服务器的客户端 IP 地址的场景。
    • ClientIP() 更适用于需要获取经过代理服务器后的客户端真实 IP 地址的场景,适用于有反向代理或负载均衡器的环境。
  3. 处理代理
    • RemoteIP() 不考虑代理的情况,只返回直接连接的客户端 IP。
    • ClientIP() 考虑代理的情况,优先从请求头获取客户端的真实 IP,适用于处理经过多个代理的请求。

获取文件上传参数

参考Gin文件上传


响应请求参数

本章介绍处理完http请求后如何响应请求,Gin框架支持以字符串、json、xml、文件等格式响应请求。

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

以字符串格式响应请求

通过String方法返回字符串。

方法定义:

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

参数说明:

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

示例:

1
2
3
4
r.GET("/news", func(c *gin.Context) { 
    aid := c.Query("aid") 
    c.String(http.StatusOK, "aid=%s", aid) 
})

提示: net/http包定义了多种常用的状态码常量,例如:http.StatusOK == 200, http.StatusMovedPermanently == 301, http.StatusNotFound == 404, http.StatusBadRequest == 400等,具体可以参考net/http包

以json格式响应请求

我们开发api接口的时候常用的格式就是json。

通过JSON方法返回json数据。

方法定义:

1
func (c *Context) JSON(code int, obj any)

参数说明:

  • code:响应的http状态码。
  • obj:响应的json数据。类型是可以序列化为json类型的数据,如:结构体对象、map[string]any。Gin框架会自动将它们序列化为JSON数据,并将它们放在请求体当中。
    • Gin框架为方便将map数据作为json响应,提供了一个快捷类型:type H map[string]anygin.Hmap[string]any类型。

示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
type User struct {
	Id   int    `form:"id" json:"id" xml:"id" uri:"id" binding:"required"`
	Name string `form:"name" json:"name" xml:"name" uri:"name"`
}

func main() { 
    r := gin.Default()
// gin.H 是 map[string]interface{}的缩写 
    r.GET("/someJSON", func(c *gin.Context) {
// 方式一:自己拼接 JSON 
        c.JSON(http.StatusOK, gin.H{"message": "Hello world!"}) // {"messgae":"Hello world!"}
    }) 
    
    r.GET("/moreJSON", func(c *gin.Context) {
// 方法二:使用结构体 
       u := models.User{Id: 123, Name: "Marry"}
		c.JSON(http.StatusOK, u)  // {"id":123,"name":"Marry"}
    }) 
    r.Run("localhost:8080") 
}

以带填充的json格式响应请求(了解)

  • 用到的方法是JSONP,英文名为:JSON with Padding,中文大概:带填充的json。

    • 只有当请求中指定查询参数callback时,才会响应带填充的json,否则,只响应json。
    • 示例:
      • 请求:http://localhost:8080/get?callback=abc,响应:abc({json数据})
        • Content-Type:application/javascript; charset=utf-8
      • 请求:http://localhost:8080/get,响应:{json数据}
        • Content-Type:application/json; charset=utf-8
  • 作用:

    • JSONP 是一种古老的跨域请求解决方案,通过 <script> 标签加载和执行 JavaScript 代码来实现数据传输,因此只适用于 GET 请求的简单跨域场景。
      • 因为要配合script标签的src属性使用,它只能发送GET请求。
      • 带填充的json格式就是JavaScript代码。
  • 具体使用:

    1. 在前端定义好回调函数名。

    2. 通过<script> 标签的 src 属性指向跨域的服务器,并包含一个查询参数 callback,其值是一个回调函数的名字。

    3. 服务器收到请求后,将数据包装在回调函数中,生成一段 JavaScript 代码,并返回给客户端。

    4. 浏览器加载并执行返回的 JavaScript 代码,从而调用回调函数,回调函数接收数据并处理。

      示例:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      
      r.GET("/get", func(c *gin.Context) {
      			data := map[string]interface{}{
      				"message": "Hello, World!",
      				"status":  "success",
      			}
      			c.JSONP(http.StatusOK, gin.H{
      				"data": data,
      			})
      		})
      

      http://localhost:8080/get?callback=abc响应:

      1
      2
      3
      4
      5
      6
      
      abc({
      	"data": {
      		"message": "Hello, World!",
      		"status": "success"
      	}
      })
      

      http://localhost:8080/get响应:

      1
      2
      3
      4
      5
      6
      
      {
      	"data": {
      		"message": "Hello, World!",
      		"status": "success"
      	}
      }
      
  • 注意事项:

    1. 存在数据安全问题:由于 JSONP 是通过执行返回的 JavaScript 代码来实现数据传输的,如果不对返回的数据进行验证,可能会执行恶意代码。

    2. 支持有限:JSONP 只支持 GET 请求,不支持其他 HTTP 方法(如 POSTPUTDELETE 等)。因此,它只适用于获取数据的场景。

    3. 推荐使用现代跨域解决方案:CORS(Cross-Origin Resource Sharing)跨域资源共享,允许浏览器向跨域服务器发送请求并接受响应。通过设置适当的 HTTP 头,服务器可以指示浏览器允许跨域请求,不受同源策略控制,报错。

      1. 就是设置对应的响应头(跨域资源共享头cors头),告诉浏览器允许跨域请求

      2. 主要的 CORS 头部:

        1. Access-Control-Allow-Origin:指定允许的源。 * 表示允许所有源。

        2. Access-Control-Allow-Methods:指定允许的 HTTP 方法。

        3. Access-Control-Allow-Headers:指定允许的请求头。

        4. Access-Control-Allow-Credentials:指示是否允许发送凭证。

        5. Access-Control-Expose-Headers:指定可以暴露的响应头。

        6. Access-Control-Max-Age:指定预检请求结果的缓存时间。

        7. 注意:以上头都只有在跨域请求中才会进行校验。

          示例:

           1
           2
           3
           4
           5
           6
           7
           8
           9
          10
          11
          12
          13
          14
          15
          16
          17
          18
          19
          20
          21
          22
          23
          24
          25
          26
          27
          28
          29
          30
          31
          32
          33
          34
          35
          36
          
          package main
          
          import (
              "github.com/gin-gonic/gin"
              "net/http"
          )
          
          func CORSMiddleware() gin.HandlerFunc {
              return func(c *gin.Context) {
                  c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
                  c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
                  c.Writer.Header().Set("Access-Control-Allow-Headers", "Origin, Content-Type, Accept")
          
                  if c.Request.Method == "OPTIONS" {
                      c.AbortWithStatus(http.StatusNoContent)
                      return
                  }
          
                  c.Next()
              }
          }
          
          func main() {
              r := gin.Default()
              r.Use(CORSMiddleware())
          
              r.GET("/data", func(c *gin.Context) {
                  data := map[string]interface{}{
                      "message": "Hello, World!",
                      "status":  "success",
                  }
                  c.JSON(http.StatusOK, data)
              })
          
              r.Run(":8080")
          }
          

          本文后面会介绍Gin中间件

      3. 在Gin框架中,Gin 官方推荐使用 gin-contrib/cors 中间件来设置 CORS 头部。

以xml格式响应请求

通过XML方法返回xml数据

方法定义:

1
func (c *Context) XML(code int, obj any)

该方法与JSON方法类似。

参数说明:

  • code:响应的http状态码。
  • obj:响应的xml数据。类型是能够序列化成xml数据的类型,如map[string]any、结构体对象。

示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
type User struct {
	Id   int    `form:"id" json:"id" xml:"id" uri:"id" binding:"required"`
	Name string `form:"name" json:"name" xml:"name" uri:"name"`
}

func main() { 
    r := gin.Default()
// gin.H 是 map[string]interface{}的缩写 
    r.GET("/someXML", func(c *gin.Context) {
        c.XML(http.StatusOK, gin.H{"message": "Hello world!"}) // <map><message>Hello world!</message></map>
    }) 
    
    r.GET("/moreXML", func(c *gin.Context) {
// 方法二:使用结构体 
       u := models.User{Id: 123, Name: "Marry"}
		c.XML(http.StatusOK, u)  // <User><id>123</id><name>Marry</name></User>
    }) 
    r.Run("localhost:8080") 
}

以文件格式响应请求

下面介绍gin框架如何响应一个文件,可以用来做文件下载。

响应文件用FileFileAttachment方法:

  • File方法:
    • 直接响应文件,Gin框架会自动根据文件类型设置MIME类型,浏览器会自动根据MEM类型I处理文件(显示文件)。
    • 只接收一个参数:为响应文件的本地系统路径。
  • FileAttachment方法:
    • 直接下载文件,会弹出下载框。文件名为给定的第二个参数。
    • 有两个参数:第一个参数与File的参数一直,第二个参数:指定文件下载的名称。

原理:FileAttachment会比File多设置一个响应头Content-Disposition:attachment; filename="filename",告诉浏览器将文件作为附加下载。

设置http响应头(设置Header)

在Gin框架中:

  • 获取请求头的方法是func (c *Context) GetHeader(key string)

  • 将请求头与结构体绑定的方法是BindHeaderShouldBindHeader

    • 二者区别是:BindHeader绑定失败,会自动响应400错误,而ShouldBind不会,需要手动响应错误。
  • 设置响应头的方法是func (c *Context) Header(key, value string)

    • 支持反复设置响应头。

    • 若键的值为空,将删除该响应头。

    • 设置响应头应该在响应前设置,响应后设置无效。

    • 源码:

      1
      2
      3
      4
      5
      6
      7
      
      func (c *Context) Header(key, value string) {
      	if value == "" {
      		c.Writer.Header().Del(key)
      		return
      	}
      	c.Writer.Header().Set(key, value)
      }
      

HTML模板渲染

前面详细介绍了gin框架响应不同类型的参数,其实还漏掉了响应html模板渲染。因为涉及到go模板渲染,就单独拿出来介绍。

Gin框架默认封装了golang内置的html/template包用于处理html模版,如果你开发的是接口服务,不提供html页面可以跳过本章内容。

作用:Gin框架内置的模板渲染功能,可以通过http请求,就可以生成安全的HTML内容,如动态网页、邮件内容、HTML报告等。同时简化了动态HTML页面的生成过程,通过设置模板目录和在路由处理函数中渲染模板,可以轻松地生成包含动态数据的网页。此功能有助于分离业务逻辑和表示层,提高代码的可维护性,并且通过自动HTML转义,提高了Web应用的安全性。

go模板渲染参考文章:《Template Introduction》

下面主要介绍Gin框架封装的html/template模板渲染与内置的html/template模板渲染的区别。

  1. 解析模板并创建模板对象

    作用Go内置Gin封装
    指定解析的模板文件func (t *Template) ParseFiles(filenames ...string) (*Template, error)func (engine *Engine) LoadHTMLFiles(files ...string)
    根据匹配模式解析模板文件(推荐func (t *Template) ParseGlob(pattern string) (*Template, error)func (engine *Engine) LoadHTMLGlob(pattern string)
  2. 渲染模板对象,改成:

    1
    
    func (c *Context) HTML(code int, name string, obj any)
    

    code:状态码。

    name:模板名称。

    obj:渲染的数据。

  3. 其他主要区别:

    • 将语法分隔符Delims、自定义模板函数映射FuncMap等方法的对象改成了模板引擎对象engine *Engine不需要我们在手动创建模板对象

    • 自定义模板函数也不需要我们手动添加(Funcs)自定义模板函数映射FuncMap

  4. 其余的模板语法,模板嵌套,模板函数,自动HTML内容转义,重写模板等使用都与内置的html/template一模一样。

  5. 示例:

    templates/default/hello.tmpl:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    
    {[ define "default/hello.tmpl" -]}
    <!DOCTYPE html>
    <html lang="en">
    <head>
    	<meta charset="UTF-8">
    	<meta http-equiv="X-UA-Compatible" content="IE=edge">
    	<meta name="viewport" content="width=device-width, initial-scale=1.0">
    	<title>Document</title>
    </head>
    <body>
    	<h1>模板渲染</h1>
    	<h3>Your message is: {[ ToUpper . ]}
    </h3>
    </body>
    </html>
    {[- end ]}
    

    main.go:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    	r := gin.Default()
    
    	r.Delims("{[", "]}")
    	r.FuncMap = template.FuncMap{
    		"ToUpper": strings.ToUpper,
    	}
    	r.LoadHTMLGlob("templates/**/*.tmpl")
    
    	r.GET("/html", func(c *gin.Context) {
    			msg := c.Query("msg")
    			c.HTML(http.StatusOK, "default/hello.tmpl", msg)
    		})
    

    启动http服务访问[http://localhost:8181/html?msg=Hello, worrld!](http://localhost:8181/html?msg=Hello, worrld!)将响应渲染之后的HTML内容

    浏览器呈现为:

    1
    2
    
    模板渲染
    Your message is: HELLO, WORRLD!
    

    响应的html源码为:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    
    <!DOCTYPE html>
    <html lang="en">
    <head>
    	<meta charset="UTF-8">
    	<meta http-equiv="X-UA-Compatible" content="IE=edge">
    	<meta name="viewport" content="width=device-width, initial-scale=1.0">
    	<title>Document</title>
    </head>
    <body>
    	<h1>模板渲染</h1>
    	<h3>Your message is: HELLO, WORRLD!
    </h3>
    </body>
    </html>
    

静态文件服务

当我们渲染的HTML文件中引用了静态文件时,就需要配置静态文件服务。以便js、css、jpg之类的静态文件能够正常响应。

在Gin框架中访问静态资源文件通常是通过设置一个静态文件服务来实现的。Gin提供了一个非常方便的方法来处理这一任务。

配置静态文件服务常用的两个方法:

  1. 映射静态资源目录:

    1
    2
    
    // 设置静态文件夹,URL路径 /static 映射到文件系统中的 assets 文件夹
        router.Static("/static", "./assets")
    

    router.Static("/static", "./static")方法的第一个参数是URL路径前缀,第二个参数是文件系统中的路径。当访问http://localhost:8080/static/css/styles.css时,Gin会在文件系统的./assets/css/styles.css路径查找文件并返回给客户端。

    注意事项:URL路径前缀建议为一个静态资源路径/static,要避免路由重复。底层是通过/static/*filepath匹配的,如果为/将匹配所有的路径,后续配置的路径将失效,Gin服务启动也会失败。

  2. 映射单个静态资源文件:

    1
    2
    
     // 可以使用 router.StaticFile 来设置单个文件的静态路径
        router.StaticFile("/favicon.ico", "./assets/favicon.ico")
    

    第一个参数:访问路径。第二个参数:文件系统中的路径。

    对于/favicon.icoURL路径,是一个特殊的路径,用于请求浏览器标签页的图标

    当我们使用浏览器访问任何网站时,浏览器会自动请求/favicon.ico路径,以便正常显示标签页图标。失败会显示一个地球。

    为了提升用户体验和网站的识别度,通常都会配置网站图标的静态资源映射。

    因此会用到映射单个静态资源文件StaticFile,注意不能使用映射静态资源目录,因为浏览器自动请求网站图标的路径是在根目录。

    提示

    • 网站图标(标签页图标)很小,格式为ico(x-icon),名为favicon,可以通过图标生成器生成。如:Favicon.ico图标在线生成器

    • 默认的网页图标路径是域名/favicon,当然也可以自定义,通过link标签:

      1
      2
      
       <!-- 定义网页的favicon 下面的href属性默认为/favicon.ico -->
          <link rel="icon" type="image/x-icon" href="//www.example.com/static/favicon.ico" >
      

Gin中间件

在Gin框架中,中间件(Middleware)指的是可以拦截http请求-响应生命周期的特殊函数,在请求-响应生命周期中可以注册多个中间件,每个中间件执行不同的功能,一个中间执行完再轮到下一个中间件执行。

通俗的讲:中间件就是匹配路由前和匹配路由后执行的一系列操作。

中间件的常见应用场景如下:

  1. 日志记录:记录每个请求的详细信息,例如请求方法、路径、状态码和处理时间。
  2. 认证和授权:检查请求是否包含有效的认证信息,决定是否允许访问资源。
  3. 错误处理:捕获和处理请求过程中发生的错误,返回统一的错误响应。
  4. 数据验证:在请求到达处理器之前对请求数据进行验证。
  5. CORS处理:处理跨域资源共享(CORS)的相关设置,允许或拒绝跨域请求。、
  6. 限流:限制客户端在一定时间内的请求数量,防止过载。

使用中间件

  1. 定义中间件

    中间件本质上是一个 Gin 处理函数(控制器函数:gin.HandlerFunc),接受 *gin.Context 作为参数。

    所以定义中间件可以定义一个函数,返回值是gin.HandlerFunc类型,即返回将*gin.Context 作为参数的函数即可。

    或者直接定义gin.HandlerFunc函数,跟定义控制器函数类似。

    例如,定义一个简单的日志中间件:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    
    func LoggerMiddleware() gin.HandlerFunc {
    	return func(c *gin.Context) {
    		// 记录请求开始时间
    		startTime := time.Now()
    
    		// 处理请求
    		c.Next()
    
    		// 记录处理时间
    		duration := time.Since(startTime)
    		log.Printf("Request %s %s took %v", c.Request.Method, c.Request.URL.Path, duration)
    
    	}
    }
    

    Next方法是调用该路由剩余的控制器函数,当剩余的控制器函数执行完毕之后,继续执行后面的代码,起到放行的作用。

    ​ 当存在多个中间件时,执行顺序依此类推。

    ​ 与之相反的方法Abort终止调用剩余的控制器函数,起到不放行的作用。

    如果没有调用以上两个方法,会依次按照注册顺序执行控制器函数。

    所有控制器函数执行完毕之后,意味着本次请求处理完毕。因此上面演示了记录处理一次请求所花费的时间。(注意:要将其注册为第一个中间件。)

  2. 使用中间件

    • 全局中间件

      可以在创建路由器时使用 Use 方法注册全局中间件,这样所有请求都会经过该中间件

      ​ 注意:Use方法可以一次性注册一个或多个中间件。

      示例:

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      
      r := gin.Default()
      
      // 注册全局中间件
      r.Use(LoggerMiddleware())
      
      // 定义路由
      r.GET("/ping", func(c *gin.Context) {
          c.JSON(200, gin.H{
              "message": "pong",
          })
      })
      
      r.Run() // 启动服务器
      
    • 路由组中间件

      也可以在特定的路由组中使用中间件,这样只有指定组经过该中间件。例如:

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      
      r := gin.Default()
      
      // 定义一个需要认证的路由组
      authGroup := r.Group("/auth")
      authGroup.Use(AuthMiddleware()) // 注册中间件
      {
          authGroup.GET("/profile", func(c *gin.Context) {
              c.JSON(200, gin.H{
                  "user": "John Doe",
              })
          })
      }
      
      r.Run() // 启动服务器
      
    • 单独使用中间件

      如果只希望在某个特定路由上使用中间件,可以直接在定义路由时使用,这样只有该路由经过中间件。例如:

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      
      r := gin.Default()
      
      // 单独为某个路由使用中间件
      r.GET("/secure", AuthMiddleware(), func(c *gin.Context) {
          c.JSON(200, gin.H{
              "message": "secure",
          })
      })
      
      r.Run() // 启动服务器
      

      前面介绍了Gin中间件的本质就是控制器函数,因此配置路由时,最后一个控制器函数前面的控制器函数都是中间件。

      因此路由组中间件还可以写成:

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      
      r := gin.Default()
      
      // 定义一个需要认证的路由组并注册中间件
      authGroup := r.Group("/auth", AuthMiddleware())
      // authGroup.Use(AuthMiddleware()) // 注册中间件
      {
          authGroup.GET("/profile", func(c *gin.Context) {
              c.JSON(200, gin.H{
                  "user": "John Doe",
              })
          })
      }
      
      r.Run() // 启动服务器
      

中间件之间共享数据

中间件间与对应控制器之间共享数据是通过上下文的Set方法和Get方法实现的,存储的是键值对,key是string类型,value是any类型。共享范围仅在本次请求中。生命周期是本次请求结束,自动销毁。Gin框架没有提供主动删除共享数据的方法,可以在请求中将共享数据的值设为nil,起到删除的效果。

示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func AuthMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		// 模拟鉴权:获取username成功表示通过
		username, f := c.GetPostForm("username")
		if !f {
			c.Abort()
			c.String(http.StatusUnauthorized, "未授权!")
			return
		}
		c.Set("username", username)
		c.Next()
	}
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
manageRouter := r.Group("/manage")
manageRouter.Use(AuthMiddleware())
{
    manageRouter.POST("/addUser", func(c *gin.Context) {
        username, _ := c.Get("username")
        c.JSON(http.StatusOK, gin.H{
            "msg":      "Add user",
            "username": username,
        })
    })
}

内置中间件

Gin 提供了一些内置中间件,常用的有:

  • Logger(): 日志中间件,记录请求日志。
    • 注意:无论Gin是debug或者release模式,都会记录请求日志。
  • Recovery(): 恢复中间件,捕获任何恐慌(panic),并返回 500 错误。
  • gin.BasicAuth(): 基本认证中间件。

使用 gin.Default() 创建的默认路由引擎,默认使用了Logger和Recovery中间件。

1
2
3
4
5
6
func Default(opts ...OptionFunc) *Engine {
	debugPrintWARNINGDefault() // 打印日志:记录使用的中间件以及go版本要求
	engine := New()
	engine.Use(Logger(), Recovery())
	return engine.With(opts...)
}

如果不想使用上面两个默认的中间件,可以使用 gin.New() 新建一个没有任何默认中间件的路由引擎。

使用方式与自定义中间件类似。例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
r := gin.New()

// 使用内置的日志和恢复中间件
r.Use(gin.Logger())
r.Use(gin.Recovery())

r.GET("/ping", func(c *gin.Context) {
    c.JSON(200, gin.H{
        "message": "pong",
    })
})

r.Run() // 启动服务器

上面示例与使用gin.Default()创建的路由引擎唯一区别是没有记录使用的中间件以及go版本要求。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func debugPrintWARNINGDefault() {
	if v, e := getMinVer(runtime.Version()); e == nil && v < ginSupportMinGoVer {
		debugPrint(`[WARNING] Now Gin requires Go 1.18+.

`)
	}
	debugPrint(`[WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

`)
}

中间件的执行顺序

Gin 中间件按照注册顺序执行。对于每个请求,所有中间件会按照注册的顺序依次执行 c.Next() 之前的代码,当一个中间件调用 c.Next() 时,控制权传递给下一个中间件,所有中间件执行完之后再按逆序执行 c.Next() 之后的代码。

中间件中使用goroutine

当在中间件或控制器函数中启动新的goroutine时,不能使用原始的上下文(c *gin.Context),因为 gin.Context 不是线程安全的。在 goroutine 中直接访问 gin.Context 的字段可能会导致竞态条件(race condition)和不可预测的行为。

中间件中使用 goroutine 的场景

  1. 日志记录:异步记录日志以减少对请求处理时间的影响。
  2. 异步处理:在响应请求后执行耗时的任务,例如发送通知邮件、清理资源等。
  3. 并发处理:在请求处理中并发执行多个任务。

在中间件中使用 goroutine 的正确方式

  1. 安全地传递上下文数据

    为了避免直接在 goroutine 中使用 gin.Context,我们可以在启动 goroutine 之前提取需要的数据,然后将这些数据传递给 goroutine。例如:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    
    func AsyncLoggerMiddleware() gin.HandlerFunc {
        return func(c *gin.Context) {
            // 提取需要的数据
            method := c.Request.Method
            path := c.Request.URL.Path
            clientIP := c.ClientIP()
    
            // 启动一个 goroutine 进行异步日志记录
            go func() {
                log.Printf("Request %s %s from %s", method, path, clientIP)
            }()
    
            // 继续处理请求
            c.Next()
        }
    }
    
  2. 请求响应之后启动协程处理任务

    可以在响应客户端后启动 goroutine 执行耗时操作,例如发送邮件通知:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    
    func AsyncTaskMiddleware() gin.HandlerFunc {
        return func(c *gin.Context) {
            // 提取需要的数据
            userEmail := c.Request.Header.Get("User-Email")
    
            // 继续处理请求
            c.Next()
    
            // 在响应客户端后启动一个 goroutine 执行异步任务
            go func() {
                // 模拟耗时任务,例如发送邮件
                time.Sleep(5 * time.Second)
                log.Printf("Email sent to %s", userEmail)
            }()
        }
    }
    
  3. 创建上下文副本

    在 Gin 中使用 c.Copy() 方法可以在 goroutine 中安全地访问 gin.Context 的数据。c.Copy() 会创建并返回 gin.Context 的一个深拷贝,这样就可以避免竞态条件的问题。

    以下是一个使用 c.Copy() 的示例,演示如何在 Gin 中间件中使用 goroutine 进行异步处理。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    
    package main
    
    import (
        "log"
        "time"
    
        "github.com/gin-gonic/gin"
    )
    
    func AsyncLoggerMiddleware() gin.HandlerFunc {
        return func(c *gin.Context) {
            // 复制上下文
            cCp := c.Copy()
    
            // 启动一个 goroutine 进行异步日志记录
            go func() {
                // 模拟日志记录的耗时操作
                time.Sleep(2 * time.Second)
                log.Printf("Request %s %s from %s", cCp.Request.Method, cCp.Request.URL.Path, cCp.ClientIP())
            }()
    
            // 继续处理请求
            c.Next()
        }
    }
    
    func AsyncTaskMiddleware() gin.HandlerFunc {
        return func(c *gin.Context) {
            // 复制上下文
            cCp := c.Copy()
    
            // 继续处理请求
            c.Next()
    
            // 在响应客户端后启动一个 goroutine 执行异步任务
            go func() {
                // 模拟耗时任务,例如发送邮件
                time.Sleep(5 * time.Second)
                userEmail := cCp.Request.Header.Get("User-Email")
                log.Printf("Email sent to %s", userEmail)
            }()
        }
    }
    
    func main() {
        r := gin.Default()
    
        // 注册中间件
        r.Use(AsyncLoggerMiddleware())
        r.Use(AsyncTaskMiddleware())
    
        // 定义路由
        r.GET("/ping", func(c *gin.Context) {
            c.JSON(200, gin.H{
                "message": "pong",
            })
        })
    
        // 启动服务器
        r.Run()
    }
    

    通过使用 c.Copy(),可以安全地在 Gin 中间件中使用 goroutine,提升应用的并发处理能力和响应效率。

注意事项

  1. 线程安全:避免在 goroutine 中直接访问 gin.Context 的字段,应该在启动 goroutine 之前提取所需数据。
  2. 资源泄漏:确保 goroutine 中的任务能够正常完成,避免因意外情况导致 goroutine 泄漏。
  3. 错误处理:在 goroutine 中处理可能发生的错误,以免影响主请求的处理流程。
  4. 使用深拷贝:确保在启动 goroutine 之前调用 c.Copy(),并在 goroutine 中使用返回的副本,而不是直接使用原始的 gin.Context

通过合理使用 goroutine,可以显著提升应用的并发处理能力,同时保持请求的快速响应。

小结

中间件在 Gin 框架中起着至关重要的作用,可以帮助开发者实现日志记录、认证授权、错误处理等常见功能。Gin 提供了灵活的中间件使用方式,可以全局、路由组或者单独路由使用中间件,满足不同的需求。通过合理使用中间件,可以大大提升应用的结构和代码复用性。


Gin文件上传

在Gin框架中,处理文件上传是一项常见的任务。

Gin是一个高效的Go语言Web框架,提供了简洁而强大的API来处理HTTP请求,包括文件上传。下面是关于如何在Gin框架中处理文件上传的相关知识和示例代码。

基本文件上传

Gin提供了方便的方法来处理单个文件的上传。以下是一个示例代码:

1
2
3
4
5
6
<form action="/admin/user/doAdd" method="post" enctype="multipart/form-data">
	用户名: <input type="text" name="username" placeholder="用户名"> <br> <br> 
    头 像1:<input type="file" name="file1"><br> <br>
    头 像2:<input type="file" name="file2"><br> <br>
    <input type="submit" value="提交">
</form>

注意:需要在上传文件的 form 表单上面需要加入 enctype=“multipart/form-data”

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package main

import (
    "fmt"
    "net/http"

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

func main() {
    r := gin.Default()
    
    r.POST("/upload", func(c *gin.Context) {
    file1, err := c.FormFile("file1")
	file2, err := c.FormFile("file2")
	if err != nil {
		c.String(http.StatusBadRequest, "Bad request: %s", err)
		return
	}

	err = c.SaveUploadedFile(file1, path.Join("./uploads", file1.Filename))
	err = c.SaveUploadedFile(file2, path.Join("./uploads", file2.Filename))
	if err != nil {
		c.String(http.StatusInternalServerError, "Could not save file: %s", err)
		return
	}

	c.String(http.StatusOK, "File %s and %s uploaded successfully.", file1.Filename, file2.Filename)
    })

    r.Run(":8080")
}

在这个例子中,Gin通过c.FormFile("file")方法获取上传的文件。

FormFile方法返回指定表单键(只能返回第一个)的描述多部分表单请求的文件部分对象。

​ 意思就是:根据多部分表单请求的文件部分的name属性获取文件部分对象multipart.FileHeader

然后使用c.SaveUploadedFile方法将文件保存到服务器的指定目录。

SaveUploadedFile方法接收两个参数:第一个:要保存的描述多部分表单请求的文件部分对象,第二个:本地系统路径(string类型)。

上面演示了如何根据不同的表单键将上传的文件保存到服务器(即处理一个或多个文件上传,name属性不同),是根据表单的name属性保存的。适用于不同的name属性

注意,上面这种情况如果存在多个相同的name属性,只能获取第一个文件。

下面介绍如何保存相同name属性的文件。适用于相同的name属性

处理多个文件上传

如果需要处理多个文件上传,name属性相同,可以使用c.MultipartForm方法。以下是示例代码:

1
2
3
4
5
6
<form action="/admin/user/doAdd" method="post" enctype="multipart/form-data">
	用户名: <input type="text" name="username" placeholder="用户名"> <br> <br> 
    头 像1:<input type="file" name="files"><br> <br>
    头 像2:<input type="file" name="files"><br> <br>
    <input type="submit" value="提交">
</form>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package main

import (
    "fmt"
    "net/http"

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

func main() {
    r := gin.Default()
    
    r.POST("/uploads", func(c *gin.Context) {
        // 从请求中解析多个文件
        form, err := c.MultipartForm()
        if err != nil {
            c.String(http.StatusBadRequest, "Bad request")
            return
        }

        files := form.File["files"]
        if len(files) != 0 {
            for _, file := range files {
                if err := c.SaveUploadedFile(file, path.Join("./uploads", file.Filename)); err != nil {
                    c.String(http.StatusInternalServerError, "Could not save file: %s", err)
                    return
                }
            }
        } else {
            c.String(http.StatusBadRequest, "Bad Request: 请上传指定文件!")
	}

        c.String(http.StatusOK, fmt.Sprintf("%d files uploaded successfully.", len(files)))
    })

    r.Run(":8080")
}

在这个例子中,首先使用c.MultipartForm方法获多部分表单数据,文件部分就在其File字段下,是map[string][]*FileHeader类型。通过表单键(name属性的值)就能获取该键下的所有文件对象,是切片类型。

最后遍历所有文件进行保存处理即可。

注意:该方法同样适用于不同的表单键,不过需要进行遍历保存。

设置处理多部分表单请求的最大内存大小

MaxMultipartMemory*gin.Engine 对象的一个字段,表示Gin框架处理multipart/form-data类型请求时,可以在内存中存储的最大数据大小。

当一个multipart/form-data请求中的数据大小超过了MaxMultipartMemory限制时,Gin框架将会自动将超出部分的数据写入到磁盘的临时文件中,而不是全部存储在内存中。这是为了防止占用过多内存导致内存溢出的问题。例如,在文件上传的情况下,较大的文件将不会全部加载到内存中,而是部分写入临时文件,从而减少内存消耗

Gin默认处理多部分表单请求的最大内存是32MiB。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package main

import (
    "fmt"
    "net/http"

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

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

    // 设置最大上传文件大小为8MB
    r.MaxMultipartMemory = 8 << 20 // 8 MiB

    r.POST("/upload", func(c *gin.Context) {
        file, err := c.FormFile("file")
        if err != nil {
            c.String(http.StatusBadRequest, "Bad request")
            return
        }

        err = c.SaveUploadedFile(file, "./uploads/" + file.Filename)
        if err != nil {
            c.String(http.StatusInternalServerError, "Could not save file")
            return
        }

        c.String(http.StatusOK, fmt.Sprintf("File %s uploaded successfully.", file.Filename))
    })

    r.Run(":8080")
}

在这个例子中,r.MaxMultipartMemory = 8 << 20 将Gin框架处理multipart/form-data类型请求的最大内存大小设置为8MiB。

8 << 20:这个表达式利用左移运算符将8左移20位,等价于8 * 2^20,即8 MiB(兆字节)。这种写法简洁明了,容易理解和记忆。

Gin会将超过部分的数据写入到磁盘的临时文件中,而不是全部存储在内存中。这样可以有效防止大文件上传时内存占用过多的问题

单位扩展

  • MiB和MB都读兆字节。
  • 二中的区别是使用的计数法不同。
    • MiB使用二进制计数法。1MiB = 1024KiB = 1,048,576 字节 = 2^20 字节
    • MB使用十进制计数法。1MB = 1000KB = 1000000 字节 = 10^6 字节
  • 通常现实生活中使用的是MB,计算机中使用的是MiB。相同数值下,MiB表示的空间更大。
    • 如购买的一块机械硬盘为2T,在计算机中显示通常小于2T。因为操作系统使用的是二进制计算方法。

使用中间件处理文件上传

通常我们会使用中间件来处理文件上传,比如验证文件类型限制文件大小等。

限制文件大小中间件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func MaxSizeAllowedMiddleware(n int64) gin.HandlerFunc {
	return func(c *gin.Context) {
		c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, n)
		if err := c.Request.ParseMultipartForm(n); err != nil {
			c.String(http.StatusRequestEntityTooLarge, "文件太大了, 不能超过%d兆字节: %s", n>>20, err)
			c.Abort()
			return
		}
		c.Next()
	}
}

验证文件类型中间件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
func ValidateFileTypeMiddleware(allowExtMap map[string]bool) gin.HandlerFunc {
	return func(c *gin.Context) {
		file, err := c.FormFile("file")
		if err != nil {
			c.String(http.StatusBadRequest, "Bad Request: %s", err)
			c.Abort()
			return
		}
		ext := path.Ext(file.Filename)
		if _, f := allowExtMap[ext]; !f {
			c.String(http.StatusBadRequest, "%q file type is not allowed", ext)
			c.Abort()
			return
		}

		c.Next()
	}
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
	r.POST("/upload", MaxSizeAllowedMiddleware(3<<20),
		ValidateFileTypeMiddleware(map[string]bool{".jpg": true, ".png": true}), 
        func(c *gin.Context) {
        	file, err := c.FormFile("file")
            if err != nil {
                c.String(http.StatusBadRequest, "Bad request: %s", err)
                return
            }

            err = c.SaveUploadedFile(file, path.Join("./uploads", file.Filename))
            if err != nil {
                c.String(http.StatusInternalServerError, "Could not save file: %s", err)
                return
            }

            c.String(http.StatusOK, "File %s uploaded successfully.", file.Filename)
    })

注意事项:

  • 上面简单演示了单文件上传时,使用中间件限制上传文件的大小,以及类型。
    • 如果是多文件上传,文件大小中间件可以适用,但文件类型中间件不适用,需要使用MultipartForm获取多部分表单请求对象,然后通过File字段遍历,判断文件类型。
  • 他们的注册顺序:
    1. 限制文件大小的中间件: 这个中间件应该先执行,因为它可以尽早地检查请求体的大小,避免浪费资源去处理超过限制的请求。
    2. 验证文件类型的中间件: 在文件大小被验证通过之后,再进行文件类型的验证。这是因为文件类型的验证通常需要解析文件内容或元数据,属于较重的操作。
    3. 通过这种顺序设置,可以确保请求被有效地过滤,并避免不必要的资源消耗。
  • 最后需要注意:如果先验证了文件类型,那么会消耗请求体内容大小,造成限制文件大小中间件失效。所以要先注册限制文件大小中间件。
  • 接口访问推荐在接口测试工具中访问,如apipost。

使用Cookie

Cookie介绍

Cookie 是在 HTTP 协议中用来存储少量数据的技术,由服务器发送到浏览器并存储在客户端。其主要作用包括:

  1. 会话管理(Session Management)

    Cookie 常用于会话管理,即在用户浏览网页期间保持用户的登录状态。常见的应用场景有:

    • 用户认证: 记录用户的登录信息,使得用户在浏览网站的不同页面时无需重新登录。
    • 购物车: 在线商店使用 Cookie 来记录用户的购物车内容。
  2. 个性化(Personalization)

    Cookie 可以用来记录用户的偏好设置,以便在用户访问网站时提供个性化的内容和体验。例如:

    • 用户界面设置: 保存用户选择的主题、语言等偏好设置。
    • 个性化广告: 根据用户的浏览历史记录和偏好,展示相关的广告内容。
  3. 跟踪和分析(Tracking and Analytics)

    Cookie 被广泛用于用户行为跟踪和数据分析,以帮助网站管理员了解用户如何使用他们的网站,并进行相应的优化。例如:

    • 流量分析: 记录用户访问的页面、停留时间等信息,用于流量统计和分析。
    • 广告跟踪: 跟踪用户点击广告的情况,以评估广告效果。
  4. 安全性(Security)

    虽然 Cookie 本身不是安全机制,但它们可以与安全机制结合使用。例如:

    • 防止跨站请求伪造(CSRF): 通过设置 HttpOnly 和 Secure 属性,增强 Cookie 的安全性,防止攻击者利用 JavaScript 窃取 Cookie 内容。
    • 令牌存储: 将安全令牌存储在 Cookie 中,以便在每次请求时进行验证。

    需要注意的是别使用cookie保存隐私数据

  5. 状态管理(State Management)

    HTTP 是无状态协议,服务器无法记录每次请求的状态。Cookie 可以帮助服务器存储和管理一些状态信息,例如:

    • 上次访问时间: 记录用户上次访问的时间,以便在用户再次访问时提供相关信息。
    • 表单数据: 存储用户在表单中输入的数据,方便在用户返回时自动填充。

Cookie的属性

Cookie 的属性决定了它的行为和存储方式,常见的属性包括:

Name 和 Value: Cookie 的名称和值。

Domain: 指定 Cookie 所属的域,只有该域及其子域可以访问 Cookie。

Path: 指定 Cookie 所属的路径,只有该路径及其子路径可以访问 Cookie。

Max-Age/Expires: 指定 Cookie 的有效期,超过这个时间后,Cookie 将被删除。

Secure: 指定 Cookie 仅在 HTTPS 连接中发送。

HttpOnly: 指定 Cookie 不能通过 JavaScript 访问,增强安全性。

SameSite: 防止跨站请求伪造(CSRF)攻击,有三个值可以选择:Strict、Lax 和 None。

补充

  • Cookie本质就是一个请求头,可以在请求头中查看Cookie。
  • 浏览器设置Cookie是通过响应头Set-Cookie设置的。
  • 查看Cookie还可以再浏览器的开发者工具的Application–>Storage中查看对应域名下的Cookie。
    • 不同浏览器可能有所不同。

在Gin中使用Cookie

  1. 设置Cookie

    在Gin中,使用SetCookie方法可以在响应中设置一个cookie:

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

    参数说明:

    • name: cookie的名称。

    • value: cookie的值。

    • maxAge: cookie的最大存活时间(以秒为单位)。

      • 有效时间,单位是秒。
      • MaxAge=0,忽略MaxAge属性,就是忽略存活时间,有效期在浏览器会话结束时删除
      • MaxAge<0 相当于删除cookie, 通常可以设置-1代表删除。
      • MaxAge>0 多少秒后cookie失效。
    • path: cookie的路径。通常设置为"/“表示整个站点。

    • domain: cookie的域。本地调试配置成 localhost , 正式上线配置成域名。

      • 子域共享Cookie

        在域名前面加点表示:该域名及其子域共享Cookie。没有只能该域访问设置的Cookie。子域无法访问。如:

        1
        2
        
        // 设置 Cookie,域为 .example.com,子域名可以共享
                c.SetCookie("username", "gin_user", 3600, "/", ".example.com", false, true)
        

        SetCookie 方法中,将域设置为 .example.com。这使得所有 example.com 及其子域(如 sub1.example.comsub2.example.com)都可以访问该 Cookie。

        注意跨不同的顶级域名、二级域名无法共享Cookie。(如 example.comanotherexample.com

        浏览器支持:确保浏览器支持跨子域名的 Cookie 设置,现代浏览器一般都支持。

    • secure: 是否仅在HTTPS请求中发送cookie。设置为false表示在HTTP和HTTPS请求中都发送。

      • 在生产环境中,建议将 Secure 设置为 true,以确保 Cookie 仅在 HTTPS 请求中发送。
    • httpOnly: 是否将cookie标记为HttpOnly。设置为true表示客户端JavaScript无法访问cookie。

  2. 获取Cookie

    使用Cookie方法可以从请求中获取cookie:

    1
    
    func (c *Context) Cookie(name string) (string, error)
    

    Cookie方法用于获取指定Cookie键的值,如果获取成功,将返回cookie的值,否则返回错误。

  3. 删除Cookie

    在Gin中,删除cookie实际上是通过设置一个过期的cookie来实现的。

    只需要将要删除的cookie的存活时间maxAge参数设置为-1,这表示将cookie立即过期,从而达到删除cookie的效果。

    1
    
    func (c *Context) SetCookie(name, value string, maxAge int, path, domain string, secure, httpOnly bool)
    
  4. 示例:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    
    package main
    
    import (
        "github.com/gin-gonic/gin"
        "net/http"
    )
    
    func main() {
        r := gin.Default()
    
        r.GET("/set_cookie", func(c *gin.Context) {
            c.SetCookie("username", "gin_user", 3600, "/", "localhost", false, true)
            c.JSON(http.StatusOK, gin.H{
                "message": "Cookie set successfully!",
            })
        })
    
        r.GET("/get_cookie", func(c *gin.Context) {
            cookie, err := c.Cookie("username")
            if err != nil {
                c.JSON(http.StatusBadRequest, gin.H{
                    "message": "Cookie not found! " + err.Error(),
                })
                return
            }
    
            c.JSON(http.StatusOK, gin.H{
                "username": cookie,
            })
        })
    
        r.GET("/delete_cookie", func(c *gin.Context) {
            c.SetCookie("username", "", -1, "/", "localhost", false, true)
            c.JSON(http.StatusOK, gin.H{
                "message": "Cookie deleted successfully!",
            })
        })
    
        r.Run(":8080")
    }
    

使用Session

Session介绍

  • 简单介绍

    Session是另一种存储少量数据的技术,不同的是Cookie保存在客户端浏览器中,而session保存在服务器上。

  • 作用

    Session是一种在客户端和服务器之间维持用户会话状态的机制。Session用于在多个请求之间保持用户的状态和信息,这在需要用户认证和个性化服务时非常有用。它可以存储用户的登录状态、购物车信息、用户偏好等。

  • 属性

    Session的属性跟Cookie的属性类似。参考Cookie的属性

    Session常用属性的默认值(Cookie需要没有):

    1
    2
    3
    4
    5
    
    	Path:     "/",
        Domain:   "localhost",
        MaxAge:   一个月,
        HttpOnly: false,
        Secure:   true,
    

    键跟值通过内置方法管理。

  • 工作流程

    当客户端(浏览器)第一次访问需要Session的页面时,服务器端会创建一个session对象,拥有唯一的一个Session ID,数据就保存在类似于key,value的键值对,然后将Session对象保存到服务器设置的存储引擎中,最后将Session ID返回到浏览器(客户)端。浏览器下次访问时会携带Session ID(cookie),找到对应的Session对象。

    在后续的请求中,客户端会携带之前收到的Session Cookie,包含唯一的Session ID。

    服务器接收到请求时,通过Session ID从存储引擎中查找对应的Session数据(键值对)。

    服务器负责Session的创建、更新和销毁。Session通常有一个生命周期,在一段时间不活动后会自动过期。

因为Gin本身没有内置的Session管理功能,所以在Gin中使用Session需要配合第三方库(中间件)实现,比较受欢迎的是gin-contrib/sessions

在Gin中使用Session

要在Gin框架中使用Session,需要借助一些中间件库实现,例如github.com/gin-contrib/sessions

gin-contrib/sessions中间件支持的存储引擎:

安装依赖

1
go get -u github.com/gin-contrib/sessions

下面介绍常用的存储引擎。

基于Cookie存储Session

基于Cookie存储Session是将Session中的数据存储在Cookie当中(与Session ID一同存放)。使用示例如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
package main

import (
    "github.com/gin-contrib/sessions"
    "github.com/gin-contrib/sessions/cookie"
    "github.com/gin-gonic/gin"
)

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

    // 使用cookie-based存储
    store := cookie.NewStore([]byte("secret"))
    r.Use(sessions.Sessions("mysession", store))

    r.GET("/login", loginHandler)
    r.GET("/logout", logoutHandler)
    r.GET("/profile", authRequired(), profileHandler)

    r.Run(":8080")
}

// 登录处理
func loginHandler(c *gin.Context) {
    	session := sessions.Default(c)
	uId := c.Query("id")
	password := c.Query("password")
	// 模拟登录认证
	if password == "123456" {
		session.Set("user", uId)
		session.Options(sessions.Options{
			MaxAge:   3600, // 一小时过期
			HttpOnly: true,
		})
		_ = session.Save()
		c.JSON(http.StatusOK, gin.H{"message": "logged in"})
	} else {
		c.JSON(http.StatusUnauthorized, gin.H{"message": "login failed"})
	}
}

// 注销处理
func logoutHandler(c *gin.Context) {
    session := sessions.Default(c)
    session.Delete("user")
    session.Save()
    c.JSON(200, gin.H{"message": "logged out"})
}

// 需要认证的处理
func authRequired() gin.HandlerFunc {
    return func(c *gin.Context) {
        session := sessions.Default(c)
        uId := session.Get("user")
        if uId == nil {
            c.JSON(401, gin.H{"message": "unauthorized"})
            c.Abort()
            return
        }
        c.Next()
    }
}

// 用户信息处理
func profileHandler(c *gin.Context) {
    session := sessions.Default(c)
    uId := session.Get("user")
    c.JSON(200, gin.H{"user": uId})
}

解释各部分功能

  • 创建Session存储引擎:使用cookie.NewStore创建一个基于Cookie的Session存储引擎。参数是一个字节切片,是Session的加密秘钥。

  • 注册Session中间件:使用sessions.Sessions("mysession", store)注册一个Session中间件,它接收两个参数,第一个是Session ID的键(string类型),第二个是存储引擎。

    • 如果要注册多个Session中间件,使用sessions.SessionsMany(sessionNames, store),它接收两个参数,第一个不同Session ID的键(string切片类型),第二个是存储引擎。一个存储引擎可以存储多个Session。
  • 获取Session对象:使用sessions.Default(c)获取Session对象,这个方法是获取只注册一个Session中间件的快捷方式。参数是Gin上下文对象。

    • 如果注册的是多个Session中间件,获取不同的Session对象,使用sessions.DefaultMany(c, "a"),这个方法是获取注册多个Session中间件的快捷方式,第一个参数是Gin上下文对象,第二个是要获取Session对象的Session ID的键。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
      store := cookie.NewStore([]byte("secret"))
      sessionNames := []string{"a", "b"}
      r.Use(sessions.SessionsMany(sessionNames, store))
    
      r.GET("/hello", func(c *gin.Context) {
        sessionA := sessions.DefaultMany(c, "a")
        sessionB := sessions.DefaultMany(c, "b")
        ...
      }
    
  • 修改Session对象属性:使用Options(sessions.Options{})方法可以修改Session的默认属性。

  • 设置Session数据:使用Set方法设置Session数据(key,value键值对)。可以重复添加数据。

  • 删除Session数据:使用Delete方法删除Session数据。参数是数据的键。

    • 理解:服务器接收到请求之后会从Cookie中自动获取Session ID,调用Default方法会根据Session ID获取对应的Session对象。数据就保存在Session对象中。因为Session ID是唯一的,所以获取的数据就是本次请求中的,可以通过数据的键进行删除。(不同用户的Session是独立的,相互之间不会干扰。)
  • 保存Session数据:使用Save方法保存Session数据。注意每次修改过Session数据之后,都需要调用此方法,数据才能生效。

  • 删除当前Session ID所有的Session数据:使用Clear方法即可删除当前Session对象所有的Session数据。

基于Redis存储Session

基于Redis存储Session就是将Session数据保存在Redis中,当浏览器发送请求时,服务器会根据Cookie中的Session ID,获取对应的Session对象。

学会了基于Cookie存储Session,那么基于Redis存储Session就很简单。我们只需要将存储引擎修改为Redis即可。其他使用方法一模一样。

创建Cookie存储引擎使用的是cookie.NewStore([]byte("secret"))方法。

创建Reds存储引擎使用的是redis.NewStore(10, "tcp", "localhost:6379", "", []byte("secret"))方法,参数介绍:

  • 第一个:Redis最大的空闲连接数。
  • 第二个:数通信协议tcp或者udp。连接类型。
  • 第三个:Redis服务器地址,格式:host:port
  • 第四个:Redis密码。
  • 最后一个:Session加密秘钥。

默认连接0号库。需要指定几号Redis数据库,使用redis.NewStoreWithDB(10, "tcp", "192.168.245.130:6379", "123456", "1", []byte("secret"))方法。

Sessions加密秘钥介绍

加密密钥的作用

  1. 数据加密
    • 因为Session的部分数据(Session ID),会保存在Cookie中响应给客户端,加密密钥用于加密这些数据,以防止客户端篡改。
    • 加密后的数据是不可读的,只有持有加密密钥的服务器才能解密和读取这些数据。
  2. 数据签名
    • 除了加密,加密密钥也用于对数据进行签名,以确保数据的完整性和真实性。
    • 签名确保数据在传输过程中未被篡改,如果数据被篡改,签名验证将失败。

使用加密秘钥

  • 在创建存储引擎的时候传入加密密钥。

  • 加密密钥是一个字节切片,可以通过一个字符串转换而来。

  • 加密秘钥是可选参数,但是必须要有一个身份验证秘钥,Sessions中间件才能起作用。

  • 只有一个加密秘钥时,用于身份验证和加密。

  • 当存在多加密秘钥时,第一个秘钥用于身份验证,后续秘钥用于加密

  • 建议使用 32 或 64 字节的身份验证密钥

  • 如果设置了加密密钥,则必须16、24 或 32 字节,以选择 AES-128、AES-192 或 AES-256 模式。否则Sessions中间件不起作用。

  • 常见的情况是设置单个身份验证密钥和可选的加密密钥。

  • 示例:

    1
    2
    3
    
    redis.NewStoreWithDB(10, "tcp",
    		"192.168.245.130:6379", "123456", "1",
    		[]byte("secret"), []byte("1234567890123456"))
    
  • 至此:

    • 设置 Session 数据时,数据会使用加密密钥加密后存储在 Cookie 中。
    • 获取 Session 数据时,数据会使用加密密钥解密。
  • 注意事项

    1. 密钥长度:使用足够长和随机的密钥来增强安全性,避免使用过短或容易猜测的密钥。
    2. 密钥管理:妥善管理加密密钥,避免泄露。可以使用环境变量或专门的密钥管理服务来存储和读取密钥。
    3. HTTPS:在生产环境中,确保使用 HTTPS 来加密传输中的数据,防止中间人攻击。

总结

本文简单介绍了Gin框架的基本使用。


参考

  1. Go gin框架入门教程