Go Web开发框架 Gin
10.1 Gin
现在的开发模式是后端只提供一份数据,然后不管是移动端还是网页端,如果想要展现出不同的效果,可以自己根据这份数据个性化构建展示效果
下载Gin package
GOPROXY=https://goproxy.cn
go get -u github.com/gin-gonic/gin
引入使用即可
import "github.com/gin-gonic/gin"
//使用Gin框架
func main() {
r := gin.Default() //返回默认的路由引擎
//访问路径以及返回
r.GET("/hello", sayhello)
//启动
//封装了http.ListenAndServe
r.Run(":8080")
}
func sayhello(c *gin.Context) {
c.JSON(200, gin.H{
"message": "Hello Golang",
})
}
10.2 RESTful架构
REST是客户端和服务器之间进行交互的时候,使用HTTP协议中的四个请求方法代表不同的动作,更多的是一种规范
GET获取资源
POST新建资源
PUT更新资源
DELETE删除资源
r.GET("/book",...)
r.POST("/book",...)
r.PUT("/book",...)
r.DELETE("/book",...)
浏览器默认只能发送GET和POST请求,如果没有通过AJAX的话,那用postman作为测试工具
10.3 Gin框架模版创建、解析和渲染
我们仍然先按照三步走操作
第一步,定义模版文件index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>posts/index</title>
</head>
<body>
{
{.title}} //在模版中使用哈希表的键来传参
</body>
</html>
第二步和第三步,解析模版和渲染模版
func main() {
//创建一个gin模版引擎路由
r := gin.Default()
//解析模版
r.LoadHTMLFiles("./templates/index.tmpl")
//创建并处理GET请求
r.GET("/index", func(c *gin.Context) {//指定路径和handler
c.HTML(http.StatusOK, "index.tmpl", gin.H{ //模版渲染,向模版中传入参数
"title": "shiyivei.com",
})
})
r.Run(":8080") //启动服务
}
渲染多个模版
可以通过define先重命名多个名字一样的模版文件,然后再解析
{
{define "posts/index.tmpl"}} //重命名
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>posts/index</title>
</head>
<body>
{
{.title}}
</body>
</html>
{
{end}} //不要忘了end
解析
//解析模版
r.LoadHTMLFiles("./templates/index.tmpl","templates/users/index.tmpl")
但是,当文件过多时,可以通过指定文件夹的方式加载所有tmpl文件
//解析模版
//r.LoadHTMLFiles("./templates/index.tmpl", "templates/users/index.tmpl")
r.LoadHTMLGlob("templates/**/*")
同样我们也需要多个path和handler来处理
func main() {
//创建一个gin模版引擎路由
r := gin.Default()
//解析模版
//r.LoadHTMLFiles("./templates/index.tmpl", "templates/users/index.tmpl")
r.LoadHTMLGlob("templates/**/*")
//创建并处理GET请求
r.GET("/posts/index", func(c *gin.Context) {
c.HTML(http.StatusOK, "posts/index.tmpl", gin.H{ //模版渲染
"title": "posts/index.tmpl",
})
})
//创建并处理GET请求
r.GET("/users/index", func(c *gin.Context) {
c.HTML(http.StatusOK, "users/index.tmpl", gin.H{ //模版渲染
"title": "users/index.tmpl",
})
})
r.Run(":8080") //启动服务
}
10.4 自定义函数
使用Gin引擎也是可以自定义函数,同样的,在解析模版之前
r.SetFuncMap(template.FuncMap{
"safe": func(str string) template.HTML { //定义匿名函数
return template.HTML(str)
},
})
然后直接常规调用就行
//创建并处理GET请求
r.GET("/users/index", func(c *gin.Context) {
c.HTML(http.StatusOK, "users/index.tmpl", gin.H{ //模版渲染
"title": "<a href='liwenzhou.com'>liwenzhou的博客</a>",
})
})
r.Run(":8080") //启动服务
10.5 静态文件处理
指的是html页面上用到的样式文件css js文件 图片等
使用/xxx路径加载文件
//如果有静态文件的话需要先加载
r.Static("/xxx", "./statics")
在tmpl文件添加链接
{
{define "users/index.tmpl"}}
<!doctype html>
<html lang="en">
<head>
<link rel="stylesheet" href="/xxx/index.css"> //添加css静态文件
<title>users/index</title>
</head>
<body>
{
{.title | safe}}
<script src="/xxx/index.js"></script> //添加js静态文件
</body>
</html>
{
{end}}
另外,Gin框架不支持使用block
使用网页上随意前端页面的一个例子
//返回从网上下载的模版
r.GET("/home", func(c *gin.Context) {
c.HTML(http.StatusOK, "home.html", nil)
})
r.Run(":8080") //启动服务
10.6 返回json
返回json格式数据的两种方式
- 前端直接请求服务器,服务器返回完整的web页面
- 前端框架拿到json格式的数据,自己进行渲染
那如果使用gin框架进行json格式文件的渲染?有两种方式,可以把map和struct返回成json格式数据如下:
func main() {
//使用gin框架的步骤
//1.引入
r := gin.Default()
//2.写访问路径和handler
//方法一,使用map
r.GET("/json", func(c *gin.Context) {
//定义一个map,填入数据
//data := map[string]interface{}{
// "name": "小王子",
// "message": "hello world",
// "age": 18,
//}
//or使用内置的map进行填写
data := gin.H{"name": "小王子", "message": "hello world", "age": 18}
c.JSON(http.StatusOK, data)
})
//方法二,使用struct
//type msg struct {
// Name string
// Message string
// Age int
//}
//使用tag标签可以把能导出的字段设置为自定义的格式,如把Name 显示成 name
type msg struct {
Name string `json:"name"`
Message string `json:"message"`
Age int `json:"age"`
}
//2.写访问路径和handler
r.GET("/another_json", func(c *gin.Context) {
data := msg{
Name: "大王子",
Message: "Hello golang",
Age: 19,
}
c.JSON(http.StatusOK, data)
})
//3.启动,写上端口号
r.Run(":8080")
//输出结果: {"name":"大王子","message":"Hello golang","age":19}
}
浏览器呈现结果
{"age":18,"message":"hello world","name":"小王子"}
10.7 gin获取querystring参数
在以上操作中,我们访问的都是路径,然后让浏览器返回该路径所对应的页面。现在我们想通过在浏览器中输入特定参数实现查询功能。首先我们需要先捕获这个输入参数
实际上这个参数对于后端来说就是一个个Map的key,当输入的参数和key所匹配时,就返回对应的value
func main() {
r := gin.Default()
r.GET("/web", func(c *gin.Context) {
//c.JSON(http.StatusOK, "ok")
//第一种方式
//name := c.Query("query")
//第二种方式
//name := c.DefaultQuery("query", "somebody")//如果制定了key就返回key对应的指定内容,否则返回默认值
//第三种方式
name, ok := c.GetQuery("query") //和上述一致的意思
if !ok {
name = "somebody"
}
//返回
c.JSON(http.StatusOK, gin.H{
"name": name,
})
})
r.Run(":8080")
}
10.8 获取form参数
form表单通过POST请求来提交
编写HTML文件
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>login</title>
</head>
<body>
<form action="/login" method="post" novalidate autocomplete="off">
<div>
<label for="username">username:</label>
<input type="text" name="username" id="username">
</div>
<div>
<label for="password">password:</label>
<input type="password" name="password" id="password">
</div>
<div>
<input type="submit" value="登录">
</div>
</form>
</body>
</html>
通过表单提交请求,此时不能成功,因为使用了GET请求
func main() {
r := gin.Default()
r.LoadHTMLFiles("./login.html")
r.GET("/login", func(c *gin.Context) {
c.HTML(http.StatusOK, "login.html", nil)
})
r.Run(":8080")
}
再写一个POST请求
r.POST("/login", func(c *gin.Context) {
username := c.PostForm("username")
password := c.PostForm("password")
c.HTML(http.StatusOK, "index.html", gin.H{
"Name": username,
"Password": password,
})
})
此时当访问http://127.0.0.1:8080/login 时,实际上触发的GET请求,当提交表单时,表单本身会触发POST请求,对应的会返回相应的内容
10.9 获取path参数
func main() {
r := gin.Default()
r.GET("/:name/:age", func(c *gin.Context) {
//获取路径参数
name := c.Param("name")
age := c.Param("age")
c.JSON(http.StatusOK, gin.H{
"name": name,
"age": age,
})
})
r.Run(":8080")
}
访问路径:http://127.0.0.1:8080/小王子/28
{"age":"28","name":"小王子"}
10.10 参数绑定
现在我们的需求是将用户传进来的参数保存起来,我们定义一个结构体来存储
ShouldBind函数可以把query/form/json不同形式提交的数据绑定到定义的结构体
type UserInfo struct {
Username string `form:"username"`
Password string `form:"password"`
}
func main() {
r := gin.Default()
r.GET("/user", func(c *gin.Context) {
//方法1:一个一个获取
获取的赋值给变量
//username := c.Query("username")
//password := c.Query("password")
//一个一个存储
把变量存起来
//user := UserInfo{username: username, password: password}
//fmt.Printf("%#v\n", user)
//方式2:使用绑定函数
var user UserInfo
err := c.ShouldBind(&user) //使用指针才能改变原来的值
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
return
} else {
//返回一个状态
fmt.Printf("%#v\n", user)
c.JSON(http.StatusOK, gin.H{
"status": "ok",
})
}
//同样的可以用POST请求
r.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
} else {
//返回一个状态
fmt.Printf("%#v\n", user)
c.JSON(http.StatusOK, gin.H{
"status": "ok",
})
}
})
r.Run(":8080")
}
10.11 gin文件上传
文件上传分为单个文件和多个文件,但是原理是一样的,即为:上传-读取-保存
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>index</title>
</head>
<body>
<form action="/upload" method="post" enctype="multipart/form-data">
<input type="file" name="f1">
<input type="submit" name="上传">
</form>
</body>
</html>
func main() {
r := gin.Default()
//先写一个用来上传文件的html页面
r.LoadHTMLFiles("./index.html")
//使用Get请求访问页面并让用户上传文件
r.GET("/index", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.html", nil)
})
//用户提交之后使用post的请求将文件处理
r.POST("/upload", func(c *gin.Context) {
//从请求中读取文件
f, err := c.FormFile("f1")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
} else {
//filePath := fmt.Sprintf("./%s",f.Filename)
filePath := path.Join("./", f.Filename) //写到本文件夹下
err = c.SaveUploadedFile(f, filePath)
if err != nil {
fmt.Printf("save uploadfile failed, err:%v", err)
}
c.JSON(http.StatusOK, gin.H{
"status": "ok",
})
}
})
r.Run(":8080")
}
10.12 gin请求重定向
HTTP重定向很容易,内外部重定向均支持
func main() {
r := gin.Default()
r.GET("/index", func(c *gin.Context) {
//c.JSON(http.StatusOK, gin.H{
// "status": "ok",
//})
//跳转百度
c.Redirect(http.StatusMovedPermanently, "http://www.baidu.com")
})
//内部重定向,转换处理请求的函数
r.GET("/a", func(c *gin.Context) {
//跳转到b对应的路由处理函数
c.Request.URL.Path = "/b"
r.HandleContext(c)
})
r.GET("/b", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "bbb",
})
})
r.Run(":8080")
}
10.13 gin路由和路由组
我们可以为每一个路径设置路由
func main() {
r := gin.Default()
//一个请求的路径和一个请求的方法对应一个handler
//常见的有四个请求方法
//获取信息
//r.GET("/index", func(c *gin.Context) {
// c.JSON(http.StatusOK, gin.H{
// "method": "GET",
// })
//})
//
创建信息,如注册成为会员
//r.POST("/index", func(c *gin.Context) {
// c.JSON(http.StatusOK, gin.H{
// "method": "POST",
// })
//})
//
删除数据
//r.DELETE("/index", func(c *gin.Context) {
// c.JSON(http.StatusOK, gin.H{
// "method": "DELETE",
// })
//})
//
修改部分数据
//r.PUT("/index", func(c *gin.Context) {
// c.JSON(http.StatusOK, gin.H{
// "method": "PUT",
// })
//})
//一种方式汇总所有请求方式
r.Any("/index", func(c *gin.Context) {
switch c.Request.Method {
case "GET":
c.JSON(http.StatusOK, gin.H{"method": "GET"})
case "POST":
c.JSON(http.StatusOK, gin.H{"method": "POST"})
case "PUT":
c.JSON(http.StatusOK, gin.H{"method": "PUT"})
case "DELETE":
c.JSON(http.StatusOK, gin.H{"method": "DELETE"})
}
})
err := r.Run(":8000")
if err != nil {
fmt.Printf("run server failed,err:%v", err)
}
}
针对那些还需未穷尽的路径,我们统一处理,如:
r.NoRoute(func(c *gin.Context) {
c.JSON(http.StatusNotFound, gin.H{"msg": "baidu.com"})
})
路由组
可以把以某段路径开头的组织为一组,公用前缀提取
//提取公共前缀
videoGroup := r.Group("/video")
{
videoGroup.GET("/index", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"msg": "/video/index"})
})
videoGroup.GET("/xxx", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"msg": "/video/xxx"})
})
videoGroup.GET("/yyy", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"msg": "/video/yyy"})
})
}
并且路由是支持嵌套的
10.14 Gin中间件
Gin框架允许开发者在处理请求的过程中,加入自己的钩子函数,这个钩子就叫中间件,中间件适合处理一些公共的业务逻辑,比如登录认证、权限校验、数据分页、记录日志、耗时统计等
钩子函数其实也是一个handler
func main() {
r := gin.Default()
r.Use(m1, m2) //全局注册中间件m1,m2
//请求先走m1再走中间件
r.GET("/index", indexHandler)
r.GET("/shop", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"msg": "shop",
})
})
r.GET("/user", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"msg": "user",
})
})
r.Run(":8082")
}
//把HandlerFunc提取出来
func indexHandler(c *gin.Context) {
fmt.Println("index")
c.JSON(http.StatusOK, gin.H{
"message": "index",
})
}
//定义一个中间件
func m1(c *gin.Context) {
//fmt.Println("m1")
//c.JSON(http.StatusOK, gin.H{
// "message": "m1",
//})
fmt.Println("m1 in...")
start := time.Now()
c.Next() //调用后续处理的函数
cost := time.Since(start)
fmt.Println(cost)
fmt.Println("m1 out...")
}
func m2(c *gin.Context) {
fmt.Println("m2 in...")
//c.Next() //调用后续处理的函数
c.Abort() //阻止调用后续处理的函数
fmt.Println("m2 out...")
}
浏览器输出结果,请求先经过m1,再到indexHandler
{"message":"m1"}{"message":"index"}
如果多个请求都用同一个中间件,则可以将其定义全局变量
r.Use(m1, m2) //全局注册中间件m1,m2
中间件的更多形式
//一个常见的中间件模版
//func authMiddleware(x *gin.Context) {
//是否登录判断
//if是登录用户
//c.next()
//else
//c.Abort()
//}
//但是更常写成闭包的形式
func authMiddleware(doCheck bool) gin.HandlerFunc {
//连接数据库
//或者一些其他的准备工作
return func(c *gin.Context) {
if doCheck {
//是否登录判断
//if是登录用户
//c.next()
//else
//c.Abort()
} else {
c.Abort()
}
}
}
还可以针对路由组定义中间件
//路由组1
xx1Group := r.Group("/xx1",authMiddleware(true))
{
xx1Group.GET("/index", func(c *gin.Context) {
c.JSON(http.StatusOK,gin.H{
"msg":"xx1Group",
})
})
}
//路由组2
xx2Group := r.Group("/xx2")
xx2Group.Use(authMiddleware(true))
{
xx2Group.GET("/index", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"msg": "xx2Group",
})
})
}
如果在中间件中判断成功,怎么传递结果其它
func m2(c *gin.Context) {
fmt.Println("m2 in...")
//在中间件中先调用set方法
c.Set("name","shiyivei")
//c.Next() //调用后续处理的函数
c.Abort() //阻止调用后续处理的函数
fmt.Println("m2 out...")
}
func indexHandler(c *gin.Context) {
fmt.Println("index")
//在处理函数中使用Get方法获取
name, ok := c.Get("name")
if !ok {
name = "匿名用户"
}
c.JSON(http.StatusOK, gin.H{
"message": name,
})
}
另外,在中间件中使用goroutine时,不能使用原始上下文中的(c *gin.Context).必须使用其只读副本(c.Copy())
go func(c.Copy())