精弘网络 2023 暑期后端 Go Web 开发基础课程 —— Gin 框架
B 站授课链接点击此处
引入
Go 内置的 net/http 包
- Go 语言内置的 net/http 包十分优秀,提供了 HTTP 客户端和服务端的实现
- 这里使用 Go 语言中的 net/http 包来编写一个简单的接收 HTTP 请求的服务端(后端)示例,具体的代码如下:
1 | package main |
将上面的代码编译运行后,在浏览器的地址栏中输入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 | package main |
将上面的代码编译运行后,在浏览器的地址栏中输入127.0.0.1:8080/hello
后回车,就能够看到和上节一样的页面:
- HTTP 状态码
- 当浏览者访问一个网页时,浏览者的浏览器会向网页所在服务器发出请求,当浏览器接收并显示网页前,此网页所在的服务器会返回一个包含 HTTP 状态码的信息头(server header)用以响应浏览器的请求
- 常见的 HTTP 状态码
- 200 - 请求成功
- 301 - 资源(网页等)被永久转移到其它 URL
- 404 - 请求的资源(网页等)不存在
- 500 - 内部服务器错误
响应返回 JSON
String
1 | r.GET("/hello", func(c *gin.Context) { |
上节课我们讲的是 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 对象——JSONObject
- 为什么普遍选择 JSON 用于前后端数据的传输
- 采用完全独立于任何程序语言的文本格式,使 JSON 成为理想的数据交换语言
- 易于人阅读和编写,键值对类型的数据结构具有良好的可读性
- 数据格式比较简单, 格式都是压缩的,占用带宽小,能有效地提升网络传输效率
- 易于解析,前端可以很方便的进行 JSON 数据的读取
- JSON 格式能够直接为后端代码使用,大大简化了前后端的代码开发量,但是完成的任务不变,且易于维护
和返回 string 一样,Gin 在 Context 里面也给我们封装了返回 JSON 的方法,下面是一个简单的 Gin 返回 JSON 的示例代码:
1 | package main |
其实除了 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 | package main |
Param 动态参数
- Param 参数获取到的数据类型也是 string 类型
- 路由形式一般写成
/user/:name/:age
,这里的:
表示后面的参数是一个占位符 - 请求的参数可以通过 URL 路径传递,
name
和age
可以通过访问/user/XiMo/3
这个路由去获取XiMo
和3
,访问/user/惜寞/4
可以获取惜寞
和4
Gin 给我们提供了 Param 方法去获取这些参数:
1 | package main |
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 | package main |
GetRawData 原始参数
我们如果想去获取前端传来的 JSON 数据类型就需要用到这个方法,但是实际上 Gin 帮我们封装了一种更简便的方式(参数绑定里的 ShouldBindJSON 方法),所以这个方法我们很少会用到,因此不专门去讲,有兴趣了解的可以看看
利用 GetRawData 方法可以获取请求体中 body 的内容,我们也是通过这种方式来获取前端给我们传来的 JSON 数据,但实际上通过这个方法我们不仅仅是可以获取 JSON ,还可以获取很多别的一些数据类型像是 xml、html 等等,这里我们仅仅是用获取 JSON 数据为例:
1 | package main |
我们将上面的代码运行起来,然后可以利用 apifox 新建一个快捷请求去查看效果:
发送请求后的结果如下:
说明我们成功的收到了前端传来的 JSON 参数并且解析到了我们的结构体和 map 上,并将其响应返回给了前端
Bind 参数绑定
下面是 Gin 的官方文档给出的介绍:
Gin 提供了两类绑定方法:
- Type - Must bind
- Methods -
Bind
,BindJSON
,BindXML
,BindQuery
,BindYAML
- 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
等效方法。
- Methods -
- Type - Should bind
- Methods -
ShouldBind
,ShouldBindJSON
,ShouldBindXML
,ShouldBindQuery
,ShouldBindYAML
- Behavior - 这些方法属于
ShouldBindWith
的具体调用。 如果发生绑定错误,Gin 会返回错误并由开发者处理错误和请求。
- Methods -
使用 Bind 方法时,Gin 会尝试根据 Content-Type 推断如何绑定。 如果你明确知道要绑定什么,可以使用 MustBindWith
或 ShouldBindWith
。
你也可以指定必须绑定的字段。 如果一个字段的 tag 加上了 binding:"required"
,但绑定时是空值, Gin 会报错。
- 我们一般不会使用 Must Bind 相关的绑定方法,因为绑定一旦发生错误,就会修改你的响应状态码,不便于你对绑定状态的控制
- 通常使用 Should Bind 方法,发生绑定错误可以自由进行处理
简单来说:
为了能够更方便的获取请求相关参数,提高开发效率,我们可以基于请求的 Content-Type
识别请求数据类型并利用反射机制自动提取请求中 Query
、Param
、Form
、JSON
等参数到结构体中。 下面的示例代码演示了 ShouldBind()
强大的功能,它能够基于请求自动提取相应类型的数据,并把值绑定到指定的结构体对象中
1 | package main |
Should Bind
会按照下面的顺序解析请求中的数据完成绑定:- 如果是
GET
请求,只使用Form
绑定(query
)
- 如果是
- 其他请求,根据
Content-Type
自动识别对应类型,匹配不上会默认使用Form
(form-data
)
- 其他请求,根据
如果你明确知道要绑定什么数据类型,推荐直接使用
ShouldBindWith
比如你明确接收到的是一个 JSON 数据,就可以使用
ShouldBindWith(&user, binding.JSON)
- 或者
ShouldBindJSON(&user)
(实际上是上面方法的缩写,通常写这个)
我们主要知道如何用 ShouldBindJSON 去获取前端发送过来的 JSON 数据就可以了
1 | package main |
路由
在 Gin 中,路由是指将 HTTP 请求映射到相应的处理函数的机制
普通路由
1 | r.GET("/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 | r.GET("/path/:id", func(c *gin.Context) {...}) // 匹配带有 id 参数的GET请求 |
通过参数路由,可以根据不同的参数值生成不同的 URL,实现对不同资源的访问。
路由组
我们可以将拥有共同 URL 前缀的路由划分为一个路由组,习惯性一对 {}
包裹同组的路由,这只是为了看着清晰,用不用 {}
包裹功能上没什么区别
通常将路由分组用在划分业务逻辑或划分 API 版本时
1 | user := r.Group("/user") |
路由组支持嵌套
1 | user := r.Group("/user") |
其实可以类比成文件夹
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 | // 对订单进行增删改查的操作 |
中间件
Gin 框架允许开发者在处理请求的过程中,加入用户自己的钩子(Hook)函数。这个钩子函数就叫中间件,中间件适合处理一些公共的业务逻辑,比如登录认证、权限校验、数据分页、记录日志、耗时统计等。
中间件本质上是一个 handler,可以作用在单路由、路由组和全局的 Engine。
定义中间件
- Gin 中的中间件必须是一个
gin.HandlerFunc
类型,其实和我们的处理函数是同样的类型。 - 下面其实就是一个中间件,只不过没有什么实际的作用:
1 | func m1(c *gin.Context) { |
我们可以把它注册到我们的路由当中:
1 | func main() { |
- 实际上访问 127.0.0.1:8080/ 的时候会返次两个响应,但是我们基本不会这么写,一次请求对应多次响应是不准确也是不安全的,仅仅是用来做一个示例
- 根据打印和响应我们也可以发现执行顺序是中间件在处理函数之前
Abort() 和 Next()
Abort()
- 会中止当前请求,不执行该语句后面的所有内容,即使后面可能处理函数都没有执行
- 注意和 return 不同,return 只结束该函数内的内容,不影响后续函数的执行
Next()
- 从当前 handler 的调用位置跳到下一个 handler 执行,执行完后续 handler(如果没有再次调用 next ),再返回上一个 next 调用位置继续往下执行
1 | func m1(c *gin.Context) { |
- 这里写了两个中间件 m1 和 m2,遇到 Next 后会跳转到下一个 Handler
- 本质上就是一个函数嵌套调用的过程
所以运行后的输出结果为:
1 | m1 in... |
如果将 m1 中的 c.Next()
换成 c.Abort()
1 | func m1(c *gin.Context) { |
运行后的输出结果为:
1 | m1 in... |
运行到 Abort() 就结束了
Set() 和 Get()
- 用于中间件(handler)之间的通信,可以是在 handler 之内,也可以是之间
- 因为中间件实际上是不同的函数,在不同的函数之间我们不能直接传递数据,想要传递数据就需要用到 Gin 给我们封装好的 Context 中的 Set 和 Get 方法
Set
:用于在请求上下文中设置数据。可以使用c.Set(key, value)
方法将某个键和对应的值存储到请求上下文中Get
:用于从请求上下文中获取数据。可以使用c.Get(key)
方法根据键获取在请求上下文中存储的值- 先 Set 再 Get
1 | type UserInfo struct { |
记录接口耗时的中间件
我们可以写一个中间件用来记录接口耗时
1 | // StatCost 是一个统计请求耗时的中间件 |
注册中间件
在 gin 框架中,我们可以为每个路由添加任意数量的中间件。
为全局路由注册
1 | func main() { |
- 我们之前所讲的
gin.Default()
实际上就默认帮我们注册了两个全局中间件Logger()
和Recovery()
- 如果不想集成这两个中间件可以使用
gin.New()
方法
为某个路由单独注册
1 | // 给 / 路由单独注册中间件(可注册多个) |
为路由组注册中间件
为路由组注册中间件有以下两种写法。
写法 1:
1 | user := r.Group("/user", StatCost) |
写法 2:
1 | user := r.Group("/user") |
通过路由组注册中间件易于定义中间件的使用范围
比如我只有 /admin 相关的路由需要鉴权,判断是不是管理员,就可以通过路由组的方式去管理,而不需要单个一个个的注册或者全局注册(没有必要给所有路由都加上该中间件)