2-掌握控制器

思考并回答以下问题:

实验介绍

本次实验重点讲解Gin框架中的核心知识,控制器(Controller),或处理器(Handler),帮助学员们深刻理解如何在Gin框架中编写GET、POST等方法的控制器函数,以及请求中的数据绑定(Binding)、如何处理HTTP响应。

知识点

  • 上下文
  • 请求
  • 表单
  • 响应

控制器

咱们首先回顾一下,在前面实验的第一个Gin应用中,我们通过一个极简的Gin应用简单提及了控制器(Controller)。它的参数包含路由、处理函数。类似下面这样。

1
2
3
app.GET("/", func(c *gin.Context) {
c.String(http.StatusOK, "Hello World!")
})

上面是个非常简单的HTTP GET请求的Hello World控制器定义,当请求时会返回Hello World给客户端。

我们不会在这里讲解第一个参数,URL路径或路由(Router),这个在后面的实验中会详细介绍。而接下来,我们会稍微重点讲解第二个参数,处理函数(Handler Function),以及其包含的唯一参数c *gin.Context上下文(Context)。

这里其实还有一个重要概念,也就是路由方法(Routing Method),在这里是以app.GET的方式被调用。它对应的是定义一个接收HTTP GET请求的路由。那么其他HTTP请求方法,例如POST、PUT、DELETE呢?没错,它们分别就是直接调用对应的大写形式的方法名,即app.POSTapp.PUTapp.DELETE。对于一些特殊的HTTP请求方法,例如HEAD、PATCH、OPTIONS等,也同样如此。我们将这些方法都统称为路由方法,它们在Gin框架中对应的接口是IRoutes,定义如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type IRoutes interface {
// 定义中间件
Use(...HandlerFunc) IRoutes

// 路由方法
Handle(string, string, ...HandlerFunc) IRoutes
Any(string, ...HandlerFunc) IRoutes
GET(string, ...HandlerFunc) IRoutes
POST(string, ...HandlerFunc) IRoutes
DELETE(string, ...HandlerFunc) IRoutes
PATCH(string, ...HandlerFunc) IRoutes
PUT(string, ...HandlerFunc) IRoutes
OPTIONS(string, ...HandlerFunc) IRoutes
HEAD(string, ...HandlerFunc) IRoutes

// 静态文件
StaticFile(string, string) IRoutes
StaticFileFS(string, string, http.FileSystem) IRoutes
Static(string, string) IRoutes
StaticFS(string, http.FileSystem) IRoutes
}

而app对应的gin.Engine类,其实最终继承了这个接口,因此可以调用这些方法。其他的关于中间件、静态文件的方法我们目前暂时不用关心,后面会详细讲解。

我们可以在路由方法对应的接口IRoutes中发现有另一个类型HandlerFunc,其实这就是处理函数对应的接口。它的定义如下。

1
type HandlerFunc func(*Context)

这也就是处理函数的基本形式,只带唯一参数*gin.Context,也就是上下文。

好了,理清楚关于控制器及其相关的概念之后,我们需要重点讲解Gin框架中的重要概念,上下文。

上下文

上下文在Gin框架中是非常重要的概念,因为Web框架中大部分概念都与它相关,包括HTTP请求、HTTP响应、表单绑定、中间件等。我们会看到,上下文在处理函数中是核心般的存在:要解析请求,得用上下文;要返回响应,得用上下文;要验证用户令牌,得用上下文。因此,了解上下文的基本原理以及如何使用,对我们学习Gin框架是很有必要的。

上下文主要是通过其中包含的公有方法(大写开头的方法)来进行操作的,其方法有几十个,很多。其方法总结下来有几大类,如下表。

其中,我们目前主要需要了解请求和响应相关的方法类别,即前4类。对所有方法感兴趣的学员可以参考上下文官方文档,以加深对该概念的掌握。

现在,为了加深对上下文概念的理解,我们将在前面实验的的代码基础上添加一部分代码。

同样,打开文件main.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
package main

import (
"errors"
"net/http"

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

func main() {
// 创建 Gin 引擎
app := gin.New()

// 定义控制器
app.GET("/", func(c *gin.Context) {
c.String(http.StatusOK, "Hello World!")
})
app.GET("/:id", func(c *gin.Context) {
// 请求路径参数 id
id := c.Param("id")

// 请求 URL 参数 query
query := c.Query("query")

// 如果参数为空,返回错误
if query == "" {
c.AbortWithError(http.StatusBadRequest, errors.New("query is empty"))
return
}

// 响应 JSON 数据
res := map[string]interface{}{
"id": id,
"query": query,
}

// 返回 200 响应
c.AbortWithStatusJSON(http.StatusOK, res)
})

// 运行 Gin 引擎
app.Run(":8080")
}

除了之前创建的Hello World控制器以外,我们在下面添加了一个新的控制器,其中:id表示URL路径动态参数,后面讲路由的时候会详细讲解。

输入完毕后,咱们打开终端,运行go run main.go,可以看到日志如下。

然后,我们在URL后面添加/hello?query=world并敲回车,可以得到如下符合预期的结果。

接下来,咱们试试不带URL参数query的情况。只在URL后面设置为/hello,会返回如下400报错页面。这也是意料之中的。

现在,咱们已经对Gin框架的上下文有一定了解,咱们可以进一步探索如何操作HTTP请求了。

请求

URL参数

在上一小节介绍上下文的内容中,我们都涉及了请求这个概念。现在我们将稍微详细讲解一下。

通常来说,很多HTTP请求(特别是GET)是会带着参数的,例如http://example.com?query=hello中的query=hello就是URL参数。就像前面展示的一样,我们可以通过c.Query来获取对应的URL参数,例如query := c.Query("query"),这是一个string类型。

当然,有些时候我们的URL参数不止是一两个,甚至可能作为数组或字典出现,这时候,就需要用到另一个Query开头的方法,QueryArray或QueryMap。

总结一下,对于URL参数,我们获取它们的方法如下。

1
2
3
4
5
6
7
8
// 获取 URL 参数(字符串)
func (c *Context) Query(key string) (value string)

// 获取 URL 参数(数组)
func (c *Context) QueryArray(key string) (values []string)

// 获取 URL 参数(字典)
func (c *Context) QueryMap(key string) (dicts map[string]string)

当然,对于开发过Gin应用的Go开发者来说,咱们还有更有效的获取URL参数的方式,也就是下面我们会介绍的数据绑定。

数据绑定

请求数据绑定(Binding)在Web框架中是非常常见的功能,对于Gin框架也不例外。Gin框架中关于表单绑定的主要关键词是Bind。上下文方法中,带有Bind的方法都是跟表单绑定相关的。

其中,又有两个关键词来表示绑定的,Must(严格绑定)和Should(按需绑定)。其区别在于:严格绑定会在绑定错误后直接返回400错误;而按需绑定则不会,只会将绑定的字段设置为空。以Bind开头的绑定方法都是严格绑定。

下面将常用的数据绑定方法列出来。

方法名
描述
BindHeader 严格绑定Header
BindJSON 严格绑定JSON数据
BindQuery 严格绑定URL参数
ShouldBindHeader 按需绑定Header
ShouldBindJSON 按需绑定JSON数据
ShouldBindQuery 按需绑定URL参数

下面我们做一个实战练习,体会一下关于Gin框架请求的知识。

在项目的main.go中添加另一个控制器,如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
app.GET("/query", func(c *gin.Context) {
// 数组参数
queryArray := c.QueryArray("array")

// 字典参数
queryMap := c.QueryMap("map")

// 响应 JSON 数据
res := map[string]interface{}{
"array": queryArray,
"map": queryMap,
}

// 返回 200 响应
c.AbortWithStatusJSON(http.StatusOK, res)
})

然后,在终端中运行go run main.go。成功启动后,在URL后面输入/query?array=1&array=2&map=a&map[a]=1&map[b]=2。可以看到如下响应数据。

可以看到,数组和字典形式的URL参数都获取到了。

接下来,我们要开始数据绑定了,这里我们将练习如何进行URL参数绑定以及POST请求的JSON绑定。

同样,在main.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
app.GET("/query-binding", func(c *gin.Context) {
// URL 参数
var bindingData struct {
Id int `form:"id" json:"id"`
Key string `form:"key" json:"key"`
}

// 绑定
if err := c.Bind(&bindingData); err != nil {
return
}

// 返回 200 响应
c.AbortWithStatusJSON(http.StatusOK, bindingData)
})
app.POST("/body-binding", func(c *gin.Context) {
// URL 参数
var bindingData struct {
Id int `json:"id"` // 没有form
Key string `json:"key"`
}

// 绑定
if err := c.Bind(&bindingData); err != nil {
return
}

// 返回 200 响应
c.AbortWithStatusJSON(http.StatusOK, bindingData)
})

打开终端,运行go run main.go。成功启动后,在URL后面输入/query-binding?id=1&key=alpha。可以看到如下响应数据。

绑定成功!

接下来,我们需要进行POST请求。

在URL中输入http://localhost:8080/body-binding

在Body标签中输入如下内容:

1
2
3
4
{
"id": 1,
"key": "alpha"
}

执行完毕后,会有相应的HTTP响应数据,如下图。

跟预期的结果一致,太棒了!

响应

概述

关于响应(Response)部分,我们需要了解的并不多,但它也非常重要,因为响应决定着最后用户或前端看到的东西。这里我们稍微讲解一下。

对于一个Web框架来说,会有很多种响应形式,可以是HTML返回给浏览器,也可以是JSON数据返回给前端(就像刚才展示的一样),或者是一个静态的文件(例如文件下载)等等。Gin框架也支持所有这些响应形式。我们在课程中将会讲解一些比较重要的响应形式。

JSON

对于RESTful API来说,JSON格式就是其响应形式。Gin中代码很简单,可以是单独的c.JSON 或者在最后调用AbortWithStatusJSON。这些在前面的部分都介绍过,就不详细展开了。

HTML

HTML形式的响应主要是通过加载HTML模版,然后调用c.HTML方法来完成前端页面渲染。这个主要是用于传统的MVC架构,由后端直接渲染模版成HTML传给浏览器将页面展现给用户。我们在后面有专门的实验来详细讲解Gin框架中的模版渲染。

实验总结

本次实验是整个Gin框架基础章节的第一个实验,也是Gin框架的重点,在后面的章节中会反复用到。我们通过实验讲解了控制器的基础概念,以及上下文的定义和如何使用,另外还介绍了请求和响应的相关要点。通过本次实验,同学们应该可以开始做很多事情了,比如构建一个可以传参的API,或者渲染一个HTML页面。接下来,我们将介绍Gin框架中的路由模块。

0%