精弘网络 2023 暑期后端 Go Web 开发基础课程 —— Gin 框架
B 站授课链接点击此处

引入

Go 内置的 net/http 包

  • Go 语言内置的 net/http 包十分优秀,提供了 HTTP 客户端和服务端的实现
  • 这里使用 Go 语言中的 net/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
34
35
36
37
package main

import (
    "fmt"
    "net/http"
)

// SayHello 函数是需要我们自己实现的一个函数,它有两个规定的参数
// 第一个参数用于给前端的响应(Response)写入数据,响应想要返回什么,就往这个参数里面写什么
// 第二个参数用于获取前端发送的请求(Request)
// Web 开发的本质就是一个请求对应一个响应的过程
func SayHello(w http.ResponseWriter, r *http.Request) {
    // fmt 包中的 Fprintln 函数是一个简单的可以往 w 里写东西的函数
    // 使用 Fprintln 函数将 "Hello 精弘!" 这句话以纯文本的形式写进 w 后返回给前端
    fmt.Fprintln(w, "Hello 精弘!")
}

func main() {
    // HandleFunc 函数接受两个参数,第一个是路径,即前端请求的 URL,另一个是回调函数,用于处理前端发送的请求
    // HandleFunc 函数是一个设置路由的函数,它的作用是将前端对 /hello 路径的请求映射到 SayHello 函数
    // 当前端访问 /hello 路径的时候,就去执行 SayHello 的函数,往响应写入 "Hello 精弘!" 后返回给前端
    http.HandleFunc("/hello", SayHello)

    // ListenAndServe 函数用于启动服务(Serve)并监听(Listen),接收两个参数
    // 第一个参数是 ip:port 格式的 string 参数,给 /hello 路径确定访问它的 ip 地址和端口号
    // 第二个参数指的是处理 HTTP 请求的处理器,填入nil表示使用默认的处理器
    // ":8080" 是简写,省略了 ip,默认为本机的所有 ip 如 127.0.0.1 就是其中一个,端口号指定为 8080
    // 在浏览器(前端)访问 127.0.0.1:8080/hello 就可以看到 SayHello 函数做出的响应了
    // 另外有一个 err 参数,如果端口被占用或者启动失败会返回错误,正常启动则返回 nil,即空(没有错误)
    err := http.ListenAndServe(":8080", nil)

    // 错误处理,如果错误不为 nil 则在终端打印错误
    if err != nil {
        fmt.Printf("http server failed, err:%v\n", err)
        return
    }
}

将上面的代码编译运行后,在浏览器的地址栏中输入127.0.0.1:8080/hello后回车,就能够看到如下页面:

Web 框架

  • 什么是 Web 框架
    • 用于进行 Web 开发的一套软件架构
    • 为 Web 应用程序提供了基础的功能
  • 优点
    • 可以利用它更容易、方便、快速的做一些事情
  • 缺点
    • 作为一套体系,它会有一些自己的规定或约束,不可能百分百的满足你的需求
  • 为什么要用 Web 框架
    • 如果从零开始,利用 Go 的基础库去搭建,过程会很繁琐
  • 主要作用
    • 简化应用开发
    • 在 Web 框架的基础上实现自己的业务逻辑
    • 框架提供基础功能
    • 只需要专注应用的业务逻辑

初识

介绍

Gin 是 Go 世界里最流行的一个 Web 框架,Github 上有 69K+ star,封装比较优雅,API 友好,源码注释比较明确,  是一个简单易用的轻量级框架,并且中文文档齐全。

安装

1、先初始化当前文件夹(如果没有初始化)

1
$ go mod init `name`

2、下载并安装 Gin
go get 是 go 安装软件包的下载命令

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

开始

将上节课的代码用 Gin 框架来实现

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
package main

import (
    "fmt"

    // 将 gin 引入到代码中
    "github.com/gin-gonic/gin"
)

// SayHello 函数是一个处理 HTTP 请求的回调函数
// 接受一个 gin 封装过的规定参数,即上下文对象 Context,它是所有请求处理器(处理请求的函数或方法)的入口参数
// Context 包含了 Request 和 ResponseWriter 两个参数,用于获取前端请求信息和返回响应
// 本质上是对于 Request 和 Response 的封装,提供了丰富的方法用于获取当前请求的上下文信息以及返回响应
func SayHello(c *gin.Context) {
    // 200 表示 HTTP 响应状态码(<=> http.StatusOK)
    // 使用 Context 的 String 函数将 "Hello 精弘!" 这句话以纯文本(字符串)的形式返回给前端
    // 实际上是对返回响应的封装
    c.String(200, "Hello 精弘!")
}

func main() {
    // gin.Default 函数会生成一个默认的 Engine(路由引擎)对象(集成了 Logger 和 Recovery 两个中间件,中间件后面的课会讲)
    // 变量名 r 是 router(路由)的一个简写
    // Engine 是 Gin 框架最重要的数据结构,它是 Gin 框架的入口,本质上是一个 Http Handler
    // 它是一个用于处理 HTTP 请求的对象,维护了一张路由表,将不同的 HTTP 请求路径映射到不同的处理函数上
    r := gin.Default()

    // r.GET 函数接受两个参数,一个是路径,即前端请求的 URL,另一个是回调函数,用于处理前端发送的请求
    // r.GET 函数将 /hello 路径添加到了 r 的路由表中,将对 /hello 路径的 GET 请求映射到 SayHello 函数上
    // 表示前端给后端的 /hello 路由发送一个 HTTP 的 GET 请求时
    // 后端会执行后面的 SayHello 函数,对前端的请求做出一个响应,给前端返回一个 "Hello 精弘!" 的字符串
    r.GET("/hello", SayHello)

    // 启动服务并监听,只接受一个 ip:port 格式的 string 参数,表示服务运行的 ip 地址和端口号
    // ":8080" 是简写,省略了 ip,表示监听本地所有 ip (如 127.0.0.1)的 8080 端口,接收并处理 HTTP 请求
    // 在浏览器(前端)访问 127.0.0.1:8080/hello 就可以看到 SayHello 函数做出的响应
    // 等价于 r.Run(),将 port 端口也省略,默认为 8080 端口
    err := r.Run(":8080")

    // 错误处理,如果错误不为 nil 则在终端打印错误
    if err != nil {
        fmt.Printf("http server failed, err:%v\n", err)
        return
    }
}

将上面的代码编译运行后,在浏览器的地址栏中输入127.0.0.1:8080/hello后回车,就能够看到和上节一样的页面:

  • HTTP 状态码
    • 当浏览者访问一个网页时,浏览者的浏览器会向网页所在服务器发出请求,当浏览器接收并显示网页前,此网页所在的服务器会返回一个包含 HTTP 状态码的信息头(server header)用以响应浏览器的请求
    • 常见的 HTTP 状态码
      • 200 - 请求成功
      • 301 - 资源(网页等)被永久转移到其它 URL
      • 404 - 请求的资源(网页等)不存在
      • 500 - 内部服务器错误

响应返回 JSON

String

1
2
3
r.GET("/hello", func(c *gin.Context) {
        c.String(200, "Hello 精弘!")
    })

上节课我们讲的是 c.String 给前端返回一个字符串,但在实际的前后端分离的开发过程中,直接使用 string 进行前后端数据传输并不便利,所以我们往往会选择使用 JSON 的数据格式进行前后端的数据传输

JSON

  • JSON 数据类型
    • JSON(JavaScript Object Notation, JS 对象简谱) 是一种轻量级的数据交换格式,用它可以来表示各种各样复杂的数据,如对象,数组,集合,以及集合的集合等数据
    • JSON 实际上就是一串字符串,只不过元素会使用特定的符号标注。 {} 双括号表示对象,[] 中括号表示数组,”” 双引号内是属性或值,: 冒号表示后者是前者的值(这个值可以是字符串、数字、也可以是另一个数组或对象)。
  • 一些常见的 JSON 格式
    • 一个 JSON 对象——JSONObject
      • {"name":"XiMo", "age":3}
      • {"name":"XiMo", "age":3,"address":{"city":HangZhou", "country":"China"}}
    • 一个 JSON 数组——JSONArray
      • ["XiMo", "惜寞"]
      • [{"name":"XiMo", "age":3}, {"name":"惜寞", "age":4}]
      • [{"name":"XiMo", "age":3, "address":{"city":"HangZhou", "country":"China"}}, {"name":"惜寞", "age": 4, "address":{"city":"JiaXing", "country":"China"}}]
    • 可以通过可视化将 JSON 数据类型格式化来查看,结构清晰,并且内容相同,字符串形式只是将空格回车给去掉了而已
    • 当然,数组可以包含对象,在对象中也可以包含数组
  • 为什么普遍选择 JSON 用于前后端数据的传输
    • 采用完全独立于任何程序语言的文本格式,使 JSON 成为理想的数据交换语言
    • 易于人阅读和编写,键值对类型的数据结构具有良好的可读性
    • 数据格式比较简单, 格式都是压缩的,占用带宽小,能有效地提升网络传输效率
    • 易于解析,前端可以很方便的进行 JSON 数据的读取
    • JSON 格式能够直接为后端代码使用,大大简化了前后端的代码开发量,但是完成的任务不变,且易于维护

和返回 string 一样,Gin 在 Context 里面也给我们封装了返回 JSON 的方法,下面是一个简单的 Gin 返回 JSON 的示例代码:

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
package main

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

func Json(c *gin.Context) {
    // 1.使用结构体,可以灵活利用 tag 对字段进行"换名"
    type UserInfo struct {
        Name     string `json:"username"`
        Age      int    `json:"age"`
        Password string `json:"-"` // 忽略该字段
    }

    user := UserInfo{
        Name:     "XiMo",
        Age:      3,
        Password: "123456",
    }

    // c.JSON 实际上是将结构体和 map 类型的变量进行序列化(将对象转换为JSON格式的字符串的过程)
    // 注意 UserInfo.Name 在序列化中变成了 "username",UserInfo.Age 变成了 "age"
    // "-" 表示忽略该字段,所以 UserInfo.Password 在序列化的时候会被忽略
    // 响应将返回:{"username": "XiMo", "age": 3}
    c.JSON(200, user)

    // // 2.使用 map
    // userMap := map[string]any{
    //     "name": "XiMo",
    //     "age":  3,
    // }

    // c.JSON(200, userMap)

    // // 3.使用 gin.H
    // // gin.H 实际上是 map[string]any 的一种快捷方式
    // c.JSON(200, gin.H{
    //     "name": "XiMo",
    //     "age":  3,
    // })
}

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

    r.GET("/json", Json)

    r.Run()
}

其实除了 JSON 和 string 类型的返回,Gin 还给我们提供了响应 XML、YAML、HTML 等的方式,但由于使用较少,所以这里不多涉及,感兴趣的可以自行查阅

获取参数

Query 查询参数

  • Query 参数是在 URL 中的一部分,用于向服务器发送额外的数据,由键值对组成,以 ? 为起始符号,键值对之间使用 & 分隔,例如:/user/search?name=XiMo&age=3
  • Query 参数常用于 HTTP GET 请求
  • 常见的地方有搜索(浏览器等等)

Gin 里面给我们封装的 Context 参数提供了丰富的方法帮我们获取 Query 参数

Query 方法 说明
Query 获取 key 对应的值,不存在返回空字符串
DefaultQuery key 不存在时返回一个默认值
GetQuery 获取 key 对应的值,并且返回 bool 标识,标识成功或者失败
QueryArray 获取 key 对应的值,值是一个字符串数组,不存在返回空字符串数组
GetQueryArray 获取 key 对应的值,并且返回 bool 标识,标识成功或者失败
QueryMap 获取 key 对应的值,值是一个字符串 map[string]string,不存在返回空
GetQueryMap 获取 key 对应的值,值是一个字符串 map[string]string,并且返回 bool 标识,标识成功或者失败

需要注意的是通过 Query 获取到的参数都是 string 类型

下面是一些代码示例:

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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
package main

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

func Query(c *gin.Context) {
    // 访问 /query?name=ximo&age=3&sex=
    // Query 获取key对应的值,不存在返回空字符串
    nameQuery := c.Query("name")
    ageQuery := c.Query("age")
    sexQuery := c.Query("sex")
    organizationQuery := c.Query("organization")
    c.JSON(200, gin.H{
        "nameQuery":         nameQuery,
        "ageQuery":          ageQuery,
        "sexQuery":          sexQuery,
        "organizationQuery": organizationQuery,
    })

    // // 访问 /query?name=ximo&age=3&sex=
    // // DefaultQuery key不存在时返回一个默认值
    // organizationDefaultQuery := c.DefaultQuery("organization", "精弘网络")
    // sexDefaultQuery := c.DefaultQuery("sex", "male")
    // nameDefaultQuery := c.DefaultQuery("name", "惜寞")
    // c.JSON(200, gin.H{
    //  "organizationDefaultQuery": organizationDefaultQuery,
    //  "sexDefaultQuery":          sexDefaultQuery,
    //  "nameDefaultQuery":         nameDefaultQuery,
    // })

    // // 访问 /query?name=ximo&age=3&sex=
    // // GetQuery 获取key对应的值,并且返回bool标识,标识成功或者失败
    // nameGetQuery, nameExist := c.GetQuery("name")
    // sexGetQuery, sexExist := c.GetQuery("sex")
    // organizationGetQuery, orgaorganizationExist := c.GetQuery("organization")
    // c.JSON(200, gin.H{
    //  "nameGetQuery":          nameGetQuery,
    //  "nameExist":             nameExist,
    //  "sexGetQuery":           sexGetQuery,
    //  "sexExist":              sexExist,
    //  "organizationGetQuery":  organizationGetQuery,
    //  "orgaorganizationExist": orgaorganizationExist,
    // })

    // // 访问 /query?name=ximo&age=3&sex=&hobby=code&hobby=sleep&hobby=
    // // QueryArray
    // hobbyQuery := c.Query("hobby")
    // hobbyQueryArray := c.QueryArray("hobby")
    // nameQueryArray := c.QueryArray("name")
    // sexQueryArray := c.QueryArray("sex")
    // organizationQueryArray := c.QueryArray("organization")
    // c.JSON(200, gin.H{
    //  "hobbyQuery":             hobbyQuery,
    //  "hobbyQueryArray":        hobbyQueryArray,
    //  "nameQueryArray":         nameQueryArray,
    //  "sexQueryArray":          sexQueryArray,
    //  "organizationQueryArray": organizationQueryArray,
    // })

    // // 访问 /query?name=ximo&age=3&sex=&hobby=code&hobby=sleep&hobby=
    // // GetQueryArray
    // hobbyGetQueryArray, hobbyExist := c.GetQueryArray("hobby")
    // nameGetQueryArray, nameExist := c.GetQueryArray("name")
    // sexGetQueryArray, sexExist := c.GetQueryArray("sex")
    // organizationGetQueryArray, orgaorganizationExist := c.GetQueryArray("organization")
    // c.JSON(200, gin.H{
    //  "hobbyGetQueryArray":        hobbyGetQueryArray,
    //  "hobbyExist":                hobbyExist,
    //  "nameGetQueryArray: ":       nameGetQueryArray,
    //  "nameExist":                 nameExist,
    //  "sexGetQueryArray":          sexGetQueryArray,
    //  "sexExist":                  sexExist,
    //  "organizationGetQueryArray": organizationGetQueryArray,
    //  "orgaorganizationExist":     orgaorganizationExist,
    // })

    // // 访问 /query?user[name]=ximo&user[age]=3&user[sex]=&user[hobby]=code&user[hobby]=sleep
    // // QueryMap
    // userQueryMap := c.QueryMap("user")
    // adminQueryMap := c.QueryMap("admin")
    // c.JSON(200, gin.H{
    //  "userQueryMap":  userQueryMap,
    //  "adminQueryMap": adminQueryMap,
    // })

    // // 访问 /query?user[name]=ximo&user[age]=3&user[sex]=&user[hobby]=code&user[hobby]=sleep
    // // GetQueryMap
    // userGetQueryMap, userExist := c.GetQueryMap("user")
    // adminGetQueryMap, adminExist := c.GetQueryMap("admin")
    // c.JSON(200, gin.H{
    //  "userGetQueryMap":  userGetQueryMap,
    //  "userExist":        userExist,
    //  "adminGetQueryMap": adminGetQueryMap,
    //  "adminExist":       adminExist,
    // })
}

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

    r.GET("/query", Query)

    r.Run(":8080")
}

Param 动态参数

  • Param 参数获取到的数据类型也是 string 类型
  • 路由形式一般写成 /user/:name/:age,这里的 : 表示后面的参数是一个占位符
  • 请求的参数可以通过 URL 路径传递,nameage 可以通过访问 /user/XiMo/3 这个路由去获取 XiMo3 ,访问 /user/惜寞/4 可以获取 惜寞4

Gin 给我们提供了 Param 方法去获取这些参数:

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
package main

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

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

    // 注意会匹配 /param/XiMo 但不会匹配 /param/ 或者 /param
    r.GET("/param/:name", func(c *gin.Context) {
        // 访问 /param/XiMo
        name := c.Param("name")

        // 不存在会返回空字符串
        age := c.Param("age")

        c.JSON(200, gin.H{
            "name": name,
            "age":  age,
        })
    })

    r.Run(":8080")
}

PostForm 表单参数

  • 和 Query 很像,不同之处在于数据不通过 URL 来传递,而是处于请求的主体当中
  • 表单参数常用于 POST 请求中

Gin 提供的 PostForm 函数与 Query 基本上一一对应的,具体情况见下表:

Query 方法 PostForm 方法 说明
Query PostForm 获取 key 对应的值,不存在返回空字符串
DefaultQuery DefaultPostForm key 不存在时返回一个默认值
GetQuery GetPostForm 获取 key 对应的值,并且返回 bool 标识,标识成功或者失败
QueryArray PostFormArray 获取 key 对应的值,值是一个字符串数组,不存在返回空字符串数组
GetQueryArray GetPostFormArray 获取 key 对应的值,并且返回 bool 标识,标识成功或者失败
QueryMap PostFormMap 获取 key 对应的值,值是一个字符串 map[string]string,不存在返回空
GetQueryMap GetPostFomMap 获取 key 对应的值,值是一个字符串 map[string]string,并且返回 bool 标识,标识成功或者失败

下面是一些代码示例:

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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
package main

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

func PostForm(c *gin.Context) {
    // PostForm 获取key对应的值,不存在返回空字符串
    // name=ximo&age=3&sex=
    namePostForm := c.PostForm("name")
    agePostForm := c.PostForm("age")
    sexPostForm := c.PostForm("sex")
    organizationPostForm := c.PostForm("organization")
    c.JSON(200, gin.H{
        "namePostForm":         namePostForm,
        "agePostForm":          agePostForm,
        "sexPostForm":          sexPostForm,
        "organizationPostForm": organizationPostForm,
    })

    // // DefaultPostForm key不存在时返回一个默认值
    // // name=ximo&age=3&sex=
    // organizationDefaultPostForm := c.DefaultPostForm("organization", "精弘网络")
    // sexDefaultPostForm := c.DefaultPostForm("sex", "male")
    // nameDefaultPostForm := c.DefaultPostForm("name", "惜寞")
    // c.JSON(200, gin.H{
    //  "organizationPostForm":        organizationPostForm,
    //  "organizationDefaultPostForm": organizationDefaultPostForm,
    //  "sexPostForm":                 sexPostForm,
    //  "sexDefaultPostForm":          sexDefaultPostForm,
    //  "namePostForm":                namePostForm,
    //  "nameDefaultPostForm":         nameDefaultPostForm,
    // })

    // // GetPostForm 获取key对应的值,并且返回bool标识,标识成功或者失败
    // // name=ximo&age=3&sex=
    // nameGetPostForm, nameExist := c.GetPostForm("name")
    // sexGetPostForm, sexExist := c.GetPostForm("sex")
    // organizationGetPostForm, orgaorganizationExist := c.GetPostForm("organization")
    // c.JSON(200, gin.H{
    //  "nameGetPostForm":         nameGetPostForm,
    //  "nameExist":               nameExist,
    //  "sexGetPostForm":          sexGetPostForm,
    //  "sexExist":                sexExist,
    //  "organizationGetPostForm": organizationGetPostForm,
    //  "orgaorganizationExist":   orgaorganizationExist,
    // })

    // // PostFormArray 获取key对应的值,值是一个字符串数组,不存在返回空字符串数组
    // // name=ximo&age=3&sex=&hobby=code&hobby=sleep&hobby=
    // hobbyPostForm := c.PostForm("hobby")
    // hobbyPostFormArray := c.PostFormArray("hobby")
    // namePostFormArray := c.PostFormArray("name")
    // sexPostFormArray := c.PostFormArray("sex")
    // organizationPostFormArray := c.PostFormArray("organization")
    // c.JSON(200, gin.H{
    //  "hobbyPostForm":             hobbyPostForm,
    //  "hobbyPostFormArray":        hobbyPostFormArray,
    //  "namePostFormArray":         namePostFormArray,
    //  "sexPostFormArray":          sexPostFormArray,
    //  "organizationPostFormArray": organizationPostFormArray,
    // })

    // // GetPostFormArray 获取key对应的值,并且返回bool标识,标识成功或者失败
    // // name=ximo&age=3&sex=&hobby=code&hobby=sleep&hobby=
    // hobbyGetPostFormArray, hobbyExist := c.GetPostFormArray("hobby")
    // nameGetPostFormArray, nameExist := c.GetPostFormArray("name")
    // sexGetPostFormArray, sexExist := c.GetPostFormArray("sex")
    // organizationGetPostFormArray, orgaorganizationExist := c.GetPostFormArray("organization")
    // c.JSON(200, gin.H{
    //  "hobbyGetPostFormArray":        hobbyGetPostFormArray,
    //  "hobbyExist":                   hobbyExist,
    //  "nameGetPostFormArray: ":       nameGetPostFormArray,
    //  "nameExist":                    nameExist,
    //  "sexGetPostFormArray":          sexGetPostFormArray,
    //  "sexExist":                     sexExist,
    //  "organizationGetPostFormArray": organizationGetPostFormArray,
    //  "orgaorganizationExist":        orgaorganizationExist,
    // })

    // // PostFormMap 获取key对应的值,值是一个字符串map[string]string,不存在返回空
    // // user[name]=ximo&user[age]=3&user[sex]=&user[hobby]=code&user[hobby]=sleep
    // userPostFormMap := c.PostFormMap("user")
    // adminPostFormMap := c.PostFormMap("admin")
    // c.JSON(200, gin.H{
    //  "userPostFormMap":  userPostFormMap,
    //  "adminPostFormMap": adminPostFormMap,
    // })

    // // GetPostFormMap 获取key对应的值,值是一个字符串map[string]string,并且返回bool标识,标识成功或者失败
    // // user[name]=ximo&user[age]=3&user[sex]=&user[hobby]=code&user[hobby]=sleep
    // userGetPostFormMap, userExist := c.GetPostFormMap("user")
    // adminGetPostFormMap, adminExist := c.GetPostFormMap("admin")
    // c.JSON(200, gin.H{
    //  "userGetPostFormMap":  userGetPostFormMap,
    //  "userExist":           userExist,
    //  "adminGetPostFormMap": adminGetPostFormMap,
    //  "adminExist":          adminExist,
    // })
}

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

    r.POST("/post-form", PostForm)

    r.Run(":8080")
}

GetRawData 原始参数

我们如果想去获取前端传来的 JSON 数据类型就需要用到这个方法,但是实际上 Gin 帮我们封装了一种更简便的方式(参数绑定里的 ShouldBindJSON 方法),所以这个方法我们很少会用到,因此不专门去讲,有兴趣了解的可以看看

利用 GetRawData 方法可以获取请求体中 body 的内容,我们也是通过这种方式来获取前端给我们传来的 JSON 数据,但实际上通过这个方法我们不仅仅是可以获取 JSON ,还可以获取很多别的一些数据类型像是 xml、html 等等,这里我们仅仅是用获取 JSON 数据为例:

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
package main

import (
    "encoding/json"
    "fmt"

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

func Raw(c *gin.Context) {
    // GetRawData 实际上是去获取 request.body 中的内容
    // 它返回两个参数,一个是获取到的 []byte 类型的 body 数据,另一个是 error 类型
    // 这里忽略了 error 的处理
    data, _ := c.GetRawData()

    // 打印 data 可以看到传过来的原始数据
    fmt.Println(data)

    // 将 []byte 转成 string 类型可以看它实际传过来的内容
    fmt.Println(string(data))

    // 注:下面是对 JSON 数据类型的处理
    // 我们可以通过 json 包里的 Unmarshal 来对 JSON 数据类型进行反序列化
    // 就是我前几节课所说的用 JSON 中的数据去给结构体和 map 类型变量赋值

    // 1.结构体,tag 在反序列化的时候依旧可用,会根据 tag 将对应的值赋给对应的键
    type UserInfo struct {
        Name string `json:"name"`
        Age  int    `json:"age"`
        Sex  string `json:"sex"`
    }

    var userStruct UserInfo

    // JSON 反序列化,会返回一个 error,这里忽略了对其的处理
    _ = json.Unmarshal(data, &userStruct)

    c.JSON(200, userStruct)

    // // 2.map
    // var userMap map[string]interface{}

    // _ = json.Unmarshal(data, &userMap)

    // //获取 JSON 中的 key,注意使用 ["key"] 获取
    // // name := userMap["name"]
    // // age := userMap["age"]
    // // sex := userMap["sex"]

    // c.JSON(200, userMap)
}

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

    r.POST("/raw", Raw)

    r.Run()
}

我们将上面的代码运行起来,然后可以利用 apifox 新建一个快捷请求去查看效果:

发送请求后的结果如下:

说明我们成功的收到了前端传来的 JSON 参数并且解析到了我们的结构体和 map 上,并将其响应返回给了前端

Bind 参数绑定

下面是 Gin 的官方文档给出的介绍:
Gin 提供了两类绑定方法:

  • Type - Must bind
    • Methods - BindBindJSONBindXMLBindQueryBindYAML
    • Behavior - 这些方法属于  MustBindWith  的具体调用。 如果发生绑定错误,则请求终止,并触发  c.AbortWithError(400, err).SetType(ErrorTypeBind)。响应状态码被设置为 400 并且  Content-Type  被设置为  text/plain; charset=utf-8。 如果您在此之后尝试设置响应状态码,Gin 会输出日志  [GIN-debug] [WARNING] Headers were already written. Wanted to override status code 400 with 422。 如果您希望更好地控制绑定,考虑使用  ShouldBind  等效方法。
  • Type - Should bind
    • Methods - ShouldBindShouldBindJSONShouldBindXMLShouldBindQueryShouldBindYAML
    • Behavior - 这些方法属于  ShouldBindWith  的具体调用。 如果发生绑定错误,Gin 会返回错误并由开发者处理错误和请求。

使用 Bind 方法时,Gin 会尝试根据 Content-Type 推断如何绑定。 如果你明确知道要绑定什么,可以使用  MustBindWith  或  ShouldBindWith

你也可以指定必须绑定的字段。 如果一个字段的 tag 加上了  binding:"required",但绑定时是空值, Gin 会报错。

  • 我们一般不会使用 Must Bind 相关的绑定方法,因为绑定一旦发生错误,就会修改你的响应状态码,不便于你对绑定状态的控制
  • 通常使用 Should Bind 方法,发生绑定错误可以自由进行处理

简单来说:
为了能够更方便的获取请求相关参数,提高开发效率,我们可以基于请求的 Content-Type 识别请求数据类型并利用反射机制自动提取请求中 QueryParamFormJSON 等参数到结构体中。 下面的示例代码演示了 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
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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
package main

import (
    "net/http"

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

type UserInfo struct {
    Name string `form:"name_form" uri:"name_uri" json:"name_form" binding:"required"` // binding:"required" tag 表示该属性值不能为空
    Age  int    `form:"age_form" uri:"age_uri" json:"age_form"`
    Sex  string `form:"sex_form" uri:"sex" json:"sex_form"`
}

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

    // 对应反射 tag form
    // 绑定 Query 示例
    // 1. /query?name_form=ximo&age_form=3&sex_form=male 正常响应
    // 2. /query?name_form=ximo&age_form=3&sex_form= 或 /query?name_form=ximo&age_form=3 正常响应
    // 3. /query?age_form=3&sex_form=male 或 /query?name_form=&age_form=3&sex_form=male 返回 error,binding:"required" tag 表示 Name 属性值不能为空
    r.GET("/query", func(c *gin.Context) {
        var user UserInfo

        // 根据请求的 Content-type 自动识别请求数据类型并利用反射机制自动提取请求中的参数到结构体中
        // 会返回一个 error 参数
        err := c.ShouldBind(&user)
        if err != nil {
            c.JSON(http.StatusBadRequest, gin.H{
                "error": err.Error(),
            })
            return
        }

        c.JSON(200, user)
    })

    // 对应反射 tag uri
    // 绑定 Param 的示例 /param/ximo/3/male
    r.POST("/param/:name_uri/:age_uri/:sex_uri", func(c *gin.Context) {
        var user UserInfo

// 获取 Param 比较特殊,不能直接通过 ShouldBind 绑定
        err := c.ShouldBindUri(&user)
        if err != nil {
            c.JSON(http.StatusBadRequest, gin.H{
                "error": err.Error(),
            })
            return
        }

        c.JSON(200, user)
    })

    // 对应反射 tag json
    // 绑定 JSON 的示例
    // 1. {"user_json": "ximo", "age_json": 3, "sex_json": male} 正常响应
    // 2. {"user_json": "ximo", "age_json": 3} 或 {"user_json": "ximo", "age_json": 3, "sex_json":""} 正常响应
    // 3. {"age_json": 3, "sex_json": male} 或 {"user_json": "", "age_json": 3, "sex_json": male}返回 error,binding:"required" tag 表示 Name 属性值不能为空
    r.POST("/json", func(c *gin.Context) {
        var user UserInfo

        err := c.ShouldBind(&user)
        if err != nil {
            c.JSON(http.StatusBadRequest, gin.H{
                "error": err.Error(),
            })
            return
        }

        c.JSON(200, user)
    })

    // 对应反射 tag form(和 Query 相同)
    // 绑定 form 表单示例
    // 1. name_form=ximo&age_form=3&sex_form=male 正常响应
    // 2. name_form=ximo&age_form=3&sex_form= 或 name_form=ximo&age_form=3 正常响应
    // 3. age_form=3&sex_form=male 或 name_form=&age_form=3&sex_form=male 返回 error,binding:"required" tag 表示 Name 属性值不能为空
    r.POST("/post-form", func(c *gin.Context) {
        var user UserInfo

        err := c.ShouldBind(&user)
        if err != nil {
            c.JSON(http.StatusBadRequest, gin.H{
                "error": err.Error(),
            })
            return
        }

        c.JSON(200, user)
    })

    r.Run()
}
  • Should Bind 会按照下面的顺序解析请求中的数据完成绑定:

      1. 如果是  GET  请求,只使用  Form  绑定(query
      1. 其他请求,根据  Content-Type  自动识别对应类型,匹配不上会默认使用  Formform-data
  • 如果你明确知道要绑定什么数据类型,推荐直接使用 ShouldBindWith

  • 比如你明确接收到的是一个 JSON 数据,就可以使用

    • ShouldBindWith(&user, binding.JSON)
    • 或者 ShouldBindJSON(&user) (实际上是上面方法的缩写,通常写这个)

我们主要知道如何用 ShouldBindJSON 去获取前端发送过来的 JSON 数据就可以了

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
package main

import (
    "net/http"

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

type UserInfo struct {
    Name string `json:"name" binding:"required"`
    Age  int    `json:"age"`
    Sex  string `json:"sex"`
}

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

    r.POST("/json", func(c *gin.Context) {
        var user UserInfo

        // 绑定 JSON,接收前端发送过来的 JSON 数据
        err := c.ShouldBindJSON(&user)
        if err != nil {
            c.JSON(http.StatusBadRequest, gin.H{
                "error": err.Error(),
            })
            return
        }

        c.JSON(200, user)
    })

    r.Run()
}

路由

在 Gin 中,路由是指将 HTTP 请求映射到相应的处理函数的机制

普通路由

1
2
3
4
r.GET("/path", func(c *gin.Context){...})
r.POST("/path", func(c *gin.Context){...})
r.PUT("/path", func(c *gin.Context){...})
r.DELETE("/path", func(c *gin.Context){...})
  • Any 方法
    • 可以匹配所有 HTTP 请求
1
r.Any("/path", func(c *gin.Context) {...})
  • NoRoute  方法
    • 用于处理找不到路由的情况,即当没有匹配到任何定义的路由时执行的处理函数
1
r.NoRoute(func(c *gin.Context) {...})
  • NoMethod 方法
    • 用于处理请求的 HTTP 方法不被允许的情况,即当请求的 HTTP 方法与路由定义的方法不匹配时执行的处理函数
1
r.NoMethod(func(c *gin.Context) {...})

参数路由

1
2
r.GET("/path/:id", func(c *gin.Context) {...}) // 匹配带有 id 参数的GET请求
r.GET("/path/*action", func(c *gin.Context) {...}) // 匹配任意路径的 GET 请求,只要是以 /path 开头

通过参数路由,可以根据不同的参数值生成不同的 URL,实现对不同资源的访问。

路由组

我们可以将拥有共同 URL 前缀的路由划分为一个路由组,习惯性一对 {} 包裹同组的路由,这只是为了看着清晰,用不用 {} 包裹功能上没什么区别
通常将路由分组用在划分业务逻辑或划分 API 版本时

1
2
3
4
5
6
7
8
9
10
11
12
13
user := r.Group("/user")
    {
        user.POST("/register", func(c *gin.Context) {...})
        user.POST("/login", func(c *gin.Context) {...})
        user.POST("/exit", func(c *gin.Context) {...})
    }

admin := r.Group("/admin")
    {
        admin.POST("/register", func(c *gin.Context) {...})
        admin.POST("/login", func(c *gin.Context) {...})
        admin.POST("/exit", func(c *gin.Context) {...})
    }

路由组支持嵌套

1
2
3
4
5
6
7
8
9
10
11
12
13
14
user := r.Group("/user")
    {
        user.POST("/register", func(c *gin.Context) {...})

        // 路由组嵌套
        login := user.Group("/login")
        {
            login.POST("/email", func(c *gin.Context) {...})
            login.POST("/phone", func(c *gin.Context) {...})
            login.POST("/password", func(c *gin.Context) {...})
        }

        user.POST("/exit", func(c *gin.Context) {...})
    }

其实可以类比成文件夹

RESTful API

REST 与技术无关,代表的是一种软件架构风格,REST 是 Representational State Transfer 的简称,中文翻译为“表征状态转移”或“表现层状态转化”。

简单来说,REST 的含义就是客户端(前端)与 Web 服务器(后端)之间进行交互的时候,使用 HTTP 协议中的 4 个请求方法代表不同的动作。

  • GET 用来获取资源
  • POST 用来新建资源
  • PUT 用来更新资源
  • DELETE 用来删除资源

只要 API 程序遵循了 REST 风格,那就可以称其为 RESTful API。目前在前后端分离的架构中,前后端基本都是通过 RESTful API 来进行交互。

例如,我们现在要编写一个管理外卖订单的系统,对一个订单进行查询、创建、更新和删除等操作,我们在编写程序的时候就要设计客户端浏览器与我们 Web 服务端交互的方式和路径。按照经验我们通常会设计成如下模式:

请求方法 URL 含义
GET /order 查询订单信息
POST /create_order 创建订单
POST /update_order 更新订单信息
POST /delete_order 删除订单

同样的需求我们按照 RESTful API 设计如下:

请求方法 URL 含义
GET /order 查询订单信息
POST /order 创建订单
PUT /order 更新订单信息
DELETE /order 删除订单

Gin 框架支持开发 RESTful API 的开发。

1
2
3
4
5
6
7
8
9
10
11
12
    // 对订单进行增删改查的操作
    // 可能的写法
    r.GET("/order", func(c *gin.Context) {})
    r.POST("/create_order", func(c *gin.Context) {})
    r.POST("/update_order", func(c *gin.Context) {})
    r.POST("/delete_order", func(c *gin.Context) {})
   
    // RESRful API 风格
    r.GET("/order", func(c *gin.Context) {...})
    r.POST("/order", func(c *gin.Context) {...})
    r.PUT("/order", func(c *gin.Context) {...})
    r.DELETE("/order", func(c *gin.Context) {...})

中间件

Gin 框架允许开发者在处理请求的过程中,加入用户自己的钩子(Hook)函数。这个钩子函数就叫中间件,中间件适合处理一些公共的业务逻辑,比如登录认证、权限校验、数据分页、记录日志、耗时统计等。
中间件本质上是一个 handler,可以作用在单路由、路由组和全局的 Engine。

定义中间件

  • Gin 中的中间件必须是一个 gin.HandlerFunc 类型,其实和我们的处理函数是同样的类型。
  • 下面其实就是一个中间件,只不过没有什么实际的作用:
1
2
3
4
5
6
7
8
func m1(c *gin.Context) {
    fmt.Println("这是一个中间件")
   
    // 中间件也可以返回响应
    c.JSON(200, gin.H{
        "msg": "中间件",
    })
}

我们可以把它注册到我们的路由当中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main() {
    r := gin.Default()

// 将 m1 作为一个中间件注册到该路由
    r.GET("/", m1, func(c *gin.Context) {
        fmt.Println("这是处理函数")

        c.JSON(200, gin.H{
            "msg": "处理函数",
        })
    })

    r.Run()
}
  • 实际上访问 127.0.0.1:8080/ 的时候会返次两个响应,但是我们基本不会这么写,一次请求对应多次响应是不准确也是不安全的,仅仅是用来做一个示例
  • 根据打印和响应我们也可以发现执行顺序是中间件在处理函数之前

Abort() 和 Next()

  • Abort()
    • 会中止当前请求,不执行该语句后面的所有内容,即使后面可能处理函数都没有执行
    • 注意和 return 不同,return 只结束该函数内的内容,不影响后续函数的执行
  • Next()
    • 从当前 handler 的调用位置跳到下一个 handler 执行,执行完后续 handler(如果没有再次调用 next ),再返回上一个 next 调用位置继续往下执行
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
func m1(c *gin.Context) {
    fmt.Println("m1 in...")

    // 跳到下一个 handler
    c.Next()

    fmt.Println("m1 out...")
}

func m2(c *gin.Context) {
    fmt.Println("m2 in...")

    // 跳到下一个 handler
    c.Next()

    fmt.Println("m2 out...")
}

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

    r.GET("/", m1, m2, func(c *gin.Context) {
        fmt.Println("/ in...")

        fmt.Println("/ out...")
    })

    r.Run()
}
  • 这里写了两个中间件 m1 和 m2,遇到 Next 后会跳转到下一个 Handler
  • 本质上就是一个函数嵌套调用的过程

  • 所以运行后的输出结果为:
1
2
3
4
5
6
m1 in...
m2 in...
/ in...
/ out...
m2 out...
m1 out...

如果将 m1 中的 c.Next() 换成 c.Abort()

1
2
3
4
5
6
7
8
func m1(c *gin.Context) {
    fmt.Println("m1 in...")

    // 终止当前请求,后续函数和代码都不会执行
    c.Abort()

    fmt.Println("m1 out...")
}

运行后的输出结果为:

1
m1 in...

运行到 Abort() 就结束了

Set() 和 Get()

  • 用于中间件(handler)之间的通信,可以是在 handler 之内,也可以是之间
  • 因为中间件实际上是不同的函数,在不同的函数之间我们不能直接传递数据,想要传递数据就需要用到 Gin 给我们封装好的 Context 中的 Set 和 Get 方法
  • Set:用于在请求上下文中设置数据。可以使用 c.Set(key, value) 方法将某个键和对应的值存储到请求上下文中
  • Get:用于从请求上下文中获取数据。可以使用 c.Get(key) 方法根据键获取在请求上下文中存储的值
  • 先 Set 再 Get
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
type UserInfo struct {
    Name string
    Age  int
}

func m1(c *gin.Context) {
    user := UserInfo{
        Name: "XiMo",
        Age:  3,
    }

// 将 user 以键值对的形式存储到 Context 中
    c.Set("user", user)
}

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

    r.GET("/", m1, func(c *gin.Context) {
        // 根据 key 返回 value(any 类型)和一个 bool 值
        // bool 值用来判断是否有这个 key
        user, ok := c.Get("user")
        fmt.Println(gin.H{
            "userExist": ok,
        })

        // 断言,会返回断言类型的值和一个 bool 值
        // bool 值用来判断是否断言成功
        // 断言失败,返回的值为断言类型的零值
        _user, _ok := user.(UserInfo)
        fmt.Println(gin.H{
            "type assertion": _ok,
        })

        fmt.Println(_user)
    })

    r.Run()
}

记录接口耗时的中间件

我们可以写一个中间件用来记录接口耗时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// StatCost 是一个统计请求耗时的中间件
func StatCost(c *gin.Context) {
    // 请求开始的时间
    start := time.Now()

// 请求前

    // 调用该请求的剩余处理程序
    c.Next()

// 请求后

    // 计算耗时
    cost := time.Since(start)

    // 日志形式打印
    log.Println(cost)
}

注册中间件

在 gin 框架中,我们可以为每个路由添加任意数量的中间件。

为全局路由注册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func main() {
    r := gin.Default()

// 全局注册 StatCost 中间件
    r.Use(StatCost)

    r.GET("/", func(c *gin.Context) {
        // 停1秒
        time.Sleep(time.Second)

        c.JSON(200, gin.H{
            "msg": "Hello 精弘!",
        })
    })

    r.Run()
}
  • 我们之前所讲的 gin.Default() 实际上就默认帮我们注册了两个全局中间件 Logger()Recovery()
  • 如果不想集成这两个中间件可以使用 gin.New() 方法

为某个路由单独注册

1
2
3
4
5
6
7
8
9
// 给 / 路由单独注册中间件(可注册多个)
r.GET("/", StatCost, func(c *gin.Context) {
// 停1秒
        time.Sleep(time.Second)

        c.JSON(200, gin.H{
            "msg": "Hello 精弘!",
        })
})

为路由组注册中间件

为路由组注册中间件有以下两种写法。
写法 1:

1
2
3
4
5
6
user := r.Group("/user", StatCost)
{
user.POST("/register", func(c *gin.Context) {...})
user.POST("/login", func(c *gin.Context) {...})
user.POST("/exit", func(c *gin.Context) {...})
}

写法 2:

1
2
3
4
5
6
7
user := r.Group("/user")
user.Use(StatCost)
{
user.POST("/register", func(c *gin.Context) {...})
user.POST("/login", func(c *gin.Context) {...})
user.POST("/exit", func(c *gin.Context) {...})
}

通过路由组注册中间件易于定义中间件的使用范围
比如我只有 /admin 相关的路由需要鉴权,判断是不是管理员,就可以通过路由组的方式去管理,而不需要单个一个个的注册或者全局注册(没有必要给所有路由都加上该中间件)