文章目录
前言
安装:
go get -u github.com/gin-gonic/gin
导入:import "github.com/gin-gonic/gin"
辅助包:import "net/http"
(使用状态码如http.StatusOK
时会用到)
注意:根据 官网 提示,需要Go语言编译器为1.13及以上版本才可以使用Gin框架
1. HTTP请求和参数解析
1.1 Engine的创建
使用gin
框架的第一步就是创建engine
,engine
为一个结构体,里面包含了路由组件、中间件、页面渲染接口、框架配置设置等相关内容。创建engine
有两种方式:
engine := gin.Default()
engine := gin.New()
这两种方式的区别在于:gin.Default()
会使用gin.New()
来创建engine
实例,但gin.Default()
会默认使用Logger
(负责打印日志) 和Recovery
(恢复程序运行中出现的panic) 中间件,而gin.New()
不使用任何中间件。
1.2 Handle处理HTTP请求
在engine中可以使用Handle方法直接对网络请求进行处理,Handle的函数签名为:func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers ...HandlerFunc) IRoutes
参数 | 含义 |
---|---|
httpMethod | http请求方法 |
relativePath | 要解析的接口 |
handlers | 处理对应请求的函数 |
简单的例子(代码1.1):
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
func main() {
engine := gin.Default()
// 请求形式:http://localhost:8080/hello?name=Alice
engine.Handle("GET", "/hello", func(ctx *gin.Context) {
path := ctx.FullPath() // 获取请求的接口
name := ctx.DefaultQuery("name", "游客") // 获取name参数
ctx.Writer.Write([]byte(fmt.Sprintf("亲爱的%v,您正在访问%v", name, path)))
})
engine.Run()
}
1.3 分类处理请求
可以根据不同的请求类型,使用指定的函数来进行请求处理。可以使用engine
调用engine.GET()
函数来处理GET请求,使用engine.POST()
函数来处理POST请求,以此类推,Gin框架封装了处理各种请求的方法。改写后的代码1.1为:
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
func main() {
engine := gin.Default()
// 请求形式:http://localhost:8080/hello?name=Alice
engine.GET("/hello", func(ctx *gin.Context) {
path := ctx.FullPath() // 获取请求的接口
name := ctx.DefaultQuery("name", "游客") // 获取name参数
ctx.Writer.Write([]byte(fmt.Sprintf("亲爱的%v,您正在访问%v", name, path)))
})
engine.Run()
}
1.4 重定向
重定向分为跳转和路由重定向。跳转就是直接对目标地址页面进行渲染并返回,并不会使用路由函数对目标地址进行处理。路由重定向是经过目标地址对应的路由函数处理后将目标页面进行渲染并返回。
// 跳转使用Redirect函数
func userRegister(ctx *gin.Context) {
// 用户注册功能
ctx.Writer.WriteString("注册成功")
// 跳转,第一个参数为状态码,第二个为目标地址
ctx.Redirect(http.StatusMovedPermanently, "/api/user/login")
}
// 路由重定向
var engine = gin.New()
func userRegister(ctx *gin.Context) {
// 用户注册功能
ctx.Writer.WriteString("注册成功")
ctx.Request.URL.Path = "/api/user/login"
engine.HandleContext(ctx) // 路由重定向
}
2. 请求参数绑定与多数据格式处理
2.1 参数绑定
当请求中包含表单数据时,可以使用context.PostForm
和context.GetPostForm
来获取。但如果表单数据较多时,PostForm
和GetPostForm
一次只能获取一个表单数据,开发效率较慢,于是我们需要使用Gin框架提供的表单实体绑定功能,将表单数据与结构体绑定。
以用户注册为例,用户在注册时会提供用户名、密码、手机三种信息,通过POST请求,这三种信息会以表单的形式发送到服务器上,我们可以使用一个结构体来接收表单数据:
type UserRegister struct {
UserName string `form:"username" binding:"required"`
Password string `form:"password" binding:"required"`
Phoen string `form:"phone" binding:"required"`
}
上述结构体中,我们通过结构体标签将结构体的字段名与表单中的字段名相对应,比如UserName
对应表单中的username
。通过binding
设置属性是否为必须(绑定)。
举个例子(代码2.1):
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"log"
)
// 结构体的个字段名首字母必须大写(大写表示可导出)
type Student struct {
Name string `form:"name"`
Age int `form:"age"`
}
func main() {
engine := gin.Default()
// http://localhost:8080/hello?name=Alice&&age=18
engine.Handle("GET", "/hello", func(ctx *gin.Context) {
var student Student
// 将GET参数与结构体student绑定
err := ctx.ShouldBindQuery(&student)
if err != nil {
log.Fatal(err.Error())
}
ctx.Writer.Write([]byte(fmt.Sprintf("%v同学,您的年龄是%v", student.Name, student.Age)))
})
engine.Run()
}
Gin里面提供了各种绑定类型,除了上面用到的ShouldBindQuery
函数,还有BindJSON
、ShouldBindJSON
等,不同的方法间有些许差异,比如BindJSON
在解析错误时会在请求头中返回一个400的状态码、ShouldBindJSON
则不会主动添加状态码,需要自己定义。
2.2 多数据格式返回
- 返回字节类型的切片:
context.Writer.Write([]byte(data))
- 返回字符串类型:
context.Writer.WriteString(data)
- 返回json格式:
// 将map变为JSON格式返回
// JSON函数为:JSON(code int, obj interface{})
// 第一个参数为响应状态码
context.JSON(200, map[string]interface{}{
"code": 200,
"message": "Success",
"data": "xxx",
})
// 将struct变为JSON格式返回
type Response struct {
Code int
Message string
Data string
}
resp := Response{Code: 1, Message: "Success", Data: "111"}
context.JSON(200, &resp)
由于Gin框架自定义了一个名为H
的类型type H map[string]interface{}
,我们可以使用H
来返回JSON值:context.JSON(200, gin.H{"code": 1, "message": "success", data: "111"})
- 返回XML类型:
context.XML(http._StatusOK_, gin.H{"code":200, "msg":"ok"})
- 返回HTML格式:
context.HTML(code int, name string, obj interface{})
(name
为html文件路径,obj
直接赋值为nil
)
如果想要在返回的HTML文件中加入一些静态资源(如图片),需要使用engine.Static(relativePath string, root string)
(relativePath
为HTTP请求的URL路径,root
为图片所在目录路径)来加载静态资源。
注意:返回HTML文件时,需要先使用
engine.LoadHTMLGlob(path string)
来载入HTML文件所在的目录。如果需要载入template
文件夹下所有HTML文件,则engine.LoadHTMLGlob("./template/*")
。
关于HTML
的obj
参数:如果要返回的HTML文件中使用了模板语法,那么可以将该参数设置为gin.H{"模板中变量名":值,}
来为HTML模板变量赋值。
补充:关于HTML模板
上述已经提到,我们会使用
engine.LoadHTMLGlob()
来加载某一目录下的所有模板,如果该目录下存在多级目录,并且每个目录对应不同的模板组,那我们该如何处理呢?
如果要加载的模板路径为./template/user/vip/
,那么可以这样配置路径:engine.LoadHTMLGlob("./template/**/**/*")
,这里面的每个**
都表示一层目录。为了防止出现多层目录下有相同文件名的模板文件(如/template/admin/index.html
、/template/default/index.html
),应给每个模板文件取个名字,做法如下:
{{ define "default/index.html" }}
// 模板内容
{{ end }}
这个时候,如果我们想渲染
/template/default/
下面的index.html
文件,而不是/template/admin/
下面的,则可以这样写:
// 假设有 /template/default/index.html /template/admin/index.html 两个模板
engin.LoadHTMLGlob("./template/**/*")
engine.GET("/news", func(ctx *gin.Context) {
// 渲染 default下面的index.html模板
ctx.HTML(200, "default/index.html", gin.H{})
})
在模板中定义变量:
{{ $var := .data }}
在模板中使用比较数值大小:
eq | 等于 |
---|---|
ne | 不等于 |
lt | 小于 |
le | 小于等于 |
gt | 大于 |
ge | 大于等于 |
条件判断:
{{ if gt .score 90 }}
<p>优秀</p>
{{ else if gt .score 60 }}
<p>及格</p>
{{ else }}
<p>不及格</p>
{{ end }}
遍历:
{{ range $key, $val := .data }}
<li>{{ $val }} </li>
{{ else }}
<li>切片中没有数据</li>
{{ end }}
模板函数:
- 预定义模板函数
len
:返回它的参数的整数类型长度
and
:and x y
表示如果x
为真则执行y
,否则只执行x
or
:or x y
表示如果x
为为真则执行x
,否则只执行y
not
:返回单个参数布尔值的否定
index
:返回第一个参数以后面参数为索引指向的值
- 自定义模板函数
// 定义一个时间戳转时间的函数
func UnixToTime(timeStamp int) string {
t := time.Unix(int64(timeStamp), 0)
return t.Format("2006-01-02 15:04:05") // 此时间模板有讲究,参考format.go
}
func main() {
engine := gin.Default()
// 以下 template.FuncMap 是在 html 模块下的
engine.SetFuncMap(template.FuncMap{"UnixToTime": UnixToTime}) // 注册模板函数(在加载模板之前)
engine.LoadHTMLGlob("./template/*") // 加载模板(在绑定路由之前)
engine.GET("/time", func(ctx *gin.Context) {
ctx.HTML(http.StatusOK, "index.html", gin.H{"timeStamp": time.Now().Unix()})
})
}
以上程序有两点注意:
- 使用
Format
函数将时间戳格式化为标准时间时,模板的编写是有相关标准的,具体请参考format.go
文件,以下列举出常用格式:
![image.png](https://img-blog.csdnimg.cn/img_convert/bc8b4ea43f4ae3cff1e4b975a0e4c6fa.png#clientId=u3a8c3c8e-8ff7-4&from=paste&height=376&id=u5f969f2f&margin=[object Object]&name=image.png&originHeight=751&originWidth=705&originalType=binary&ratio=1&size=98251&status=done&style=none&taskId=uefb4cc9b-cd7d-4a29-99f2-3dc914fbcf2&width=352.5)
- 使用
template.FuncMap
时,template
是html
模块下的。
3. 路由组
在实际项目中,都会采取模块化开发。同一模块内的功能接口往往会有相同的接口前缀,如:
// 用户模块
注册:/api/user/register
登录:/api/user/login
用户信息:/api/user/info
以上模块可以这么写(代码3.1):
func main() {
engine := gin.Default()
// 用户模块
// 注册:/api/user/register
// 登录:/api/user/login
// 用户信息:/api/user/info
routerGroup := engine.Group("/api/user")
routerGroup.POST("/register", userRegister)
routerGroup.POST("/login", userLogin)
routerGroup.GET("/info", userInfo)
engine.Run("localhost:8080")
}
func userRegister(ctx *gin.Context) {
// 用户注册功能
}
func userLogin(ctx *gin.Context) {
// 用户登录功能
}
func userInfo(ctx *gin.Context) {
// 查询用户信息功能
}
如果模块较多,也可以使用语句块包裹来突显不同:
func main() {
router := gin.Default()
//v1组路由
v1:=router.Group("/v1")
{
v1.GET("/login", loginEndpoint)
v1.GET("/submit", submitEndpoint)
v1.GET("/read", readEndpoint)
}
//v2组路由
v2:=router.Group("/v2")
{
v2.GET("/login", loginEndpoint)
v2.GET("/submit", submitEndpoint)
v2.GET("/read", readEndpoint)
}
router.Run()
}
4. 中间件
4.1 中间件的基本使用
在实际的业务开发过程中,会有很多通用的功能,比如日志记录、权限验证等,这些功能对系统中所有业务都适用,和具体的业务没有关联。为了使整个系统的架构更清晰、耦合度更低,我们可以将这些通用的功能抽离出来单独进行开发,封装成一个个小组件,这些组件被称为“中间件”。
- Gin框架的
Recovery
中间件
func Recovery() HandlerFunc {
return RecoveryWithWriter(DefaultErrorWriter)
}
- Gin框架的
Logger
中间件
func Logger() HandlerFunc {
return LoggerWithConfig(LoggerConfig{})
}
- Gin使用中间件
func Default() *Engine {
debugPrintWARNINGDefault()
engine := New()
engine.Use(Logger(), Recovery()) // 使用中间件
return engine
}
中间件需要满足两个要求:
- 是一个函数
- 返回值为
HandlerFunc
类型
我们可以自定义一个中间件来实现对每次请求的类型和URL的打印(代码4.1):
// 中间件
func RequestInfo() gin.HandlerFunc {
return func(ctx *gin.Context) {
now := time.Now()
timeInfo := fmt.Sprintf("%v-%v-%v %v-%v-%v", now.Year(), int(now.Month()), now.Day(),
now.Hour(), now.Minute(), now.Second())
fmt.Printf("[%v] %v - %v\n", timeInfo, ctx.Request.Method, ctx.FullPath())
}
}
func main() {
engine := gin.New()
engine.Use(RequestInfo()) // 使用中间件
routerGroup := engine.Group("/api/user")
routerGroup.POST("/register", userRegister)
routerGroup.POST("/login", userLogin)
routerGroup.GET("/info", userInfo)
engine.Run("localhost:8080")
}
上述中间件的效果如下:
实际上,根据engine.Use
函数的签名func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes
可知,只需将HandlerFunc
类型作为参数传入即可。代码4.1中间件的使用为engine.User(RequestInfo())
,而不是engine.User(RequestInfo)
,是因为RequestInfo
函数的返回值才是HandlerFunc
类型。又因为Gin框架中HandlerFunc
类型定义为type HandlerFunc func(*Context)
,可知HandlerFunc
类型实际上就是参数为*Context
的函数。所以,上述中间件还可以这样写(代码4.2):
func RequestInfo(ctx *gin.Context) {
now := time.Now()
timeInfo := fmt.Sprintf("%v-%v-%v %v-%v-%v", now.Year(), int(now.Month()), now.Day(),
now.Hour(), now.Minute(), now.Second())
fmt.Printf("[%v] %v - %v\n", timeInfo, ctx.Request.Method, ctx.FullPath())
}
func main() {
engine := gin.New()
engin.Use(RequestInfo) // 使用中间件
routerGroup.POST("/register", userRegister)
}
除了对整体使用中间件,还可以对特定请求使用中间件(代码4.3):
engine.GET("/login", 中间件1, 中间件2, ..., 处理函数)
以GET函数为例,其签名为func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes
, 它的第二参数为不定长参数,并且参数类型为HandlerFunc
,而中间件函数类型也为HandlerFunc
,也就是说,对特定路由的处理函数其实也是一个中间件,只不过它作用于特定路由,不对整体生效。代码4.3中对路由的处理会按照从左到右
的顺序,先用中间件1处理,之后是中间件2,最后才是处理函数。
对路由组使用中间件(代码4.4):
routerGroup := engine.Group("/api/user", 中间件)
{
routerGroup.GET("/login", Login)
routerGroup.POST("/register", register)
}
接下来,我们来看看**Context.Next()**的使用。中间件总是作用于请求的处理函数之前,如果我们需要使用中间件将请求处理的结果打印出来,那么我们需要在中间件中就执行处理函数,该操作可以通过context.Next()
函数来实现。中间件中,在context.Next()
之前的部分会先执行,遇到context.Next()
后程序会跳转执行处理函数,处理函数执行完成后会跳转回来去执行context.Next()
函数之后的操作。如果想使用中间件打印出请求的执行状态码(代码4.5):
func RequestInfo(ctx *gin.Context) {
now := time.Now()
timeInfo := fmt.Sprintf("%v-%v-%v %v-%v-%v", now.Year(), int(now.Month()), now.Day(),
now.Hour(), now.Minute(), now.Second())
fmt.Printf("[%v] %v - %v", timeInfo, ctx.Request.Method, ctx.FullPath())
ctx.Next() // 跳转到处理函数
fmt.Printf("StatusCode:%v", ctx.Writer.Status()) // 获取响应状态码
}
**Context.Abort()**的方法用于终止其它所有中间件的执行。如果在某一中间件里使用Abort
函数,那么该中间执行完成后,它后面的所有中间件都不会执行。
4.2 中间件和控制器共享数据
在中间件中可以使用context.Set()
函数来设置数据,然后在其它中间件中使用context.Get()
获取数据,具体用法为(代码4.6):
func middleWareOne(ctx *gin.Context) {
//Set函数的签名: func (c *Context) Set(key string, value interface{})
ctx.Set("userName", "张三")
}
func HandleFunction(ctx *gin.Context) {
// Get函数的签名:func (c *Context) Get(key string) (value interface{}, exists bool)
userName, _ := ctx.Get("userName")
if val, ok := userName.(string); ok {
ctx.string(200, "用户列表: " + val)
} else {
ctx.string(200, "获取用户失败")
}
}
如果在中间件中使用协程,则不能直接使用原本的context
需要先使用Copy
函数进行复制,然后使用副本,如(代码4.7):
func MiddleTwo(ctx *gin.Context) {
fmt.Println("第二个中间件开始")
ctxCopy := ctx.Copy() // 复制一个副本
go func() {
userName, _ := ctxCopy.Get("userName") // 使用副本
if val, ok := userName.(string); ok {
fmt.Println("用户列表:" + val)
} else {
fmt.Println("获取用户列表失败")
}
}()
ctx.Next()
fmt.Println("第二个中间件结束")
}
5. 文件上传
1. 文件上传
需要在上传文件的form
表单上面加入enctype="multipart/form-data"
:
<form method="post" action="/api/user/upload" enctype="multipart/form-data">
<input type="file" name="face" />
<input type="submit" value="提交" />
</form>
上传文件:
func Upload(ctx *gin.Context) {
file, err := ctx.FormFile("face") // 获取文件
if err != nil {
ctx.String("文件获取失败")
return
}
dst := "./static/upload/" + file.Filename // 文件保存路径
ctx.SaveUploadedFile(file, dst) // 将文件file保存到dst路径下
ctx.String("文件上传成功")
}
2. 相同名字的多文件上传
engine.POST("/upload", func(ctx *gin.Context) {
form, _ := ctx.MultipartForm()
files := form.File["face[]"] // face为表单中文件上传框的name属性值
for _, file := range files {
c.SaveUploadedFile(file, dst)
}
})
6. Cookie和Session
6.1 设置Cookie
设置Cookie
使用SetCookie
函数:func (c *Context) SetCookie(name, value string, maxAge int, path, domain string, secure, httpOnly bool)
name
:cookie名, value
:cookie值,maxAge
:cookie的生命周期(单位是秒),path
:cookie路径,domain
:cookie路径的作用域,secure
:为true表示cookie在http中无效,在https中生效,httpOnly
:若为true则通过脚本将无法获取cookie信息,防止xss攻击产生。
如:ctx.SetCookie("userName", "张三", 1200, "/", "localhost", false, true)
6.2 获取Cookie
获取Cookie使用context.Cookie(name string)
函数,name
为Cookie名,如:context.Cookie("userName")
。
6.3 Session
Gin官方没有给我们提供Session相关文档,这个时候我们可以使用第三方的Session中间件来实现,第三方库地址为:
https://github.com/gin-contrib/sessionsgin-contrib/sessions
中间件支持的存储引擎:
- cookie
- memstore
- redis
- memcached
- mongodb
导入和基本用法:
import "github.com/gin-contrib/sessions"
import "github.com/gin-contrib/sessions/cookie"
func main() {
engine := gin.Default()
// 创建基于cookie的存储引擎,secret是用于加密的秘钥
store := cookie.NewStore([]byte("secret"))
// 配置session中间件,store是存储引擎,也可以换成别的
engine.Use(sessions.Sessions("mysession", store))
engine.GET("/user", func(ctx *gin.Context) {
session := sessions.Default(ctx)
session.Get("userName") // 获取cookie
session.Set("userName", "111") // 设置cookie
session.Save() // 设置完值后要保存
})
}
注意:上述程序创建的Cookie名为
mysession
。