楔子
这次我们来学习一下Go语言中的一个Web框架,虽然我们之前介绍了net/http,但对于开发一个完整的项目来说,标准库net/http还是不太方便的。尽管它的基本功能都有了,但对于快速、高效的开发一个项目还是不太适合的,所以我们需要一个功能更加强大的Web框架。
而Go语言中的第三方Web框架有很多,我们即将学习的是gin框架。而选择这个框架的原因有几下几点:
gin是一个老牌框架, 社区庞大;性能非常优秀;学习简单;
如果有喜欢名侦探柯南的小伙伴的话,那么看到这个框架的名字一定很熟悉,这不是最惨反派琴酒吗?琴酒你特么好惨啊。
那么下面就来学习gin这个框架吧,并感受它的魅力。
gin 初识
在使用之前我们首先要进行安装,安装很简单,直接:go get -u github.com/gin-gonic/gin 即可。但是因为一些众所周知的原因,你在下载时候会面临一些困难,于是我们可以考虑使用代理。比如:七牛云、阿里云等等。
# 开启 go module
go env -w GO111MODULE=on
# 设置代理
go env -w GOPROXY=https://goproxy.cn,direct
# 下载 gin,由于设置了代理,你会发现下载速度非常非常快
go get -u github.com/gin-gonic/gin
下面我们就来写一个服务,返回一个 "hello world" 吧。
package main
import (
"github.com/gin-gonic/gin"
"log"
)
func main() {
router := gin.Default() // 生成默认的路由器
// 注册一个路由, 使用GET方法, 里面一个 路径字符串 和 一个函数
router.GET("/hello", func(context *gin.Context) {
// 返回一个字符串, 可以调用 context.String()方法
context.String(200, "hello world")
})
// 启动服务, 指定 ip 和 端口
if err := router.Run("localhost:22333"); err != nil {
log.Fatal(err)
}
}
点击右键执行即可,注意:如果你发现运行的时候提示如下信息:cannot find module providing package github.com/gin-gonic/gin: working directory is not part of a module,那么你需要在当前目录执行 go mod init gin;如果执行完之后再启动的时候又出现了:$GOPATH/go.mod exists but should not,那么将当前目录从 GOPATH 中移除即可。
我们访问:localhost:22333/hello,会看到返回的内容,这里就不截图了,可以自己打开页面访问一下。当然除了浏览器,你也可以使用postman进行测试。
那么如果我们想返回json格式的字符串呢?
package main
import (
"github.com/gin-gonic/gin"
"log"
)
func jsonHandler(context *gin.Context) {
// 返回json格式通过 gin.H, 里面的写类似于map的键值对, 类型可以任意
context.JSON(200, gin.H{
"name": "夏色祭",
"age": 16,
"gender": "female",
"hobby": []string{"斯哈斯哈", "呼吸"},
})
}
func main() {
router := gin.Default() // 生成默认的路由器
router.GET("/hello", func(context *gin.Context) {
context.String(200, "hello world")
})
router.GET("/json", jsonHandler)
if err := router.Run("localhost:22333"); err != nil {
log.Fatal(err)
}
}
访问 localhost:22333/json 即可看到返回的json数据,怎么样是不是很简单呢。如果我们访问了一个不存在的路由,那么会返回一个 404 page not found。
关于路由,记得在 net/http 中我们说过,如果定义的是:/hello,那么页面中只能通过 /hello 访问,/hello/ 是访问不到的;如果定义的是:/hello/,那么在页面中 /hello 和 /hello/ 都是可以访问。
而在gin里面,无论定义的路由是 /hello 还是 /hello/,在页面中都可以使用 /hello 和 /hello/ 进行访问。
我们在启动服务的时候,是通过 router.Run() 的方式启动的,但底层其实还是依赖了 net/http。
func main() {
router := gin.Default()
router.GET("/hello", func(context *gin.Context) {
context.String(200, "hello world")
})
router.GET("/json/", jsonHandler)
// 这种方式启动也是可以的, 事实上 router.Run 底层也是这么做的, 只是多了一些输出
if err := http.ListenAndServe("localhost:22333", router); err != nil {
log.Fatal(err)
}
}
所以我们看到这个 router 是不是有点像net/http 里面的多路复用器呢?因此它们本质是一样的。
当然我们也可以自己定义服务器的配置:
func main() {
router := gin.Default()
router.GET("/hello", func(context *gin.Context) {
context.String(200, "hello world")
})
router.GET("/json/", jsonHandler)
s := http.Server{
Addr: "localhost:22333",
Handler: router,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20,
}
if err := s.ListenAndServe(); err != nil {
log.Fatal(err)
}
}
gin router
gin 框架中采用的路由库是 httprouter,我们后面会分析它的源码。目前为止我们只见到了 GET 请求,而其它的请求也是类似的。
router.GET("/someGet", getting)
router.POST("/somePost", posting)
router.PUT("/somePut", putting)
router.DELETE("/someDelete", deleting)
router.PATCH("/somePatch", patching)
router.HEAD("/someHead", head)
router.OPTIONS("/someOptions", options)
Gin 的路由支持 GET、POST、PUT、DELETE、PATCH、HEAD、OPTIONS 请求,同时还有一个 Any 函数,可以同时支持以上的所有请求。如果需要一个路由支持多种请求,那么就可以使用 Any,演示一下:
package main
import (
"github.com/gin-gonic/gin"
"log"
"net/http"
)
func anyHandler(context *gin.Context) {
// context.Request 就是net/http 中的 *http.Request
if context.Request.Method == "GET" {
// http.StatusOK 是一个常量, 值为200
context.String(http.StatusOK, "你访问了GET请求")
} else if context.Request.Method == "POST" {
context.String(http.StatusOK, "你访问了POST请求")
} else {
context.String(http.StatusOK, "你访问了GET、POST请求之外的请求")
}
}
func main() {
router := gin.Default()
router.GET("/any/", anyHandler)
if err := router.Run("localhost:22333"); err != nil {
log.Fatal(err)
}
}
此外,gin 还提供了一个 NoRoute,意思是没有匹配到符合条件的路由是会走这个路由。
package main
import (
"github.com/gin-gonic/gin"
"log"
"net/http"
)
func main() {
router := gin.Default()
router.NoRoute(func(context *gin.Context) {
context.String(http.StatusNotFound, "你要找到页面去火星了······")
})
if err := router.Run("localhost:22333"); err != nil {
log.Fatal(err)
}
}
在页面上输入一个url,会先匹配我们定义的路由,如果都无法匹配,那么就会走 NoRoute。
路由参数
gin 的路由来自 httprouter 库,因此 httprouter 库具有的功能 gin 也具有,只不过 gin 不支持路由的正则表达式。
路径参数
路径参数通过 Context 的 Param 方法获取。
package main
import (
"github.com/gin-gonic/gin"
"log"
)
func main() {
router := gin.Default()
router.GET("/girl/:name", func(context *gin.Context) {
name := context.Param("name")
context.String(200, "你输入了%q", name)
})
if err := router.Run("localhost:22333"); err != nil {
log.Fatal(err)
}
}
使用一个冒号加上一个参数名即可组成路径参数,可以使用 context.Param 方法读取相应的值,结果是一个字符串。而访问的时候可以通过:/girl/xxx 来访问,代码中的 name 就是对应的 xxx。
请求: localhost:22333/girl/夏色祭
返回: 你输入了"夏色祭"
除了调用 context.Param 返回指定的路径参数,还可以使用 context.Params 返回所有的路径参数。
package main
import (
"github.com/gin-gonic/gin"
"log"
)
func main() {
router := gin.Default()
router.GET("/girl/:name1/:name2/:name3", func(context *gin.Context) {
names := context.Params
context.String(200, "你输入了%v", names)
})
if err := router.Run("localhost:22333"); err != nil {
log.Fatal(err)
}
}
测试一下:
请求: http://localhost:22333/girl/夏色祭/神乐七奈/碧居结衣
返回: 你输入了[{name1 夏色祭} {name2 神乐七奈} {name3 碧居结衣}]
所以:如果获取单个路径参数的值,那么可以通过 context.Param("key"),获取所有的路径参数,可以使用 context.Params,那么这两者之间的关系是怎么样的呢?
首先里面的 context.Params 是一个 Param 类型的切片。
type Context struct {
writermem responseWriter
Request *http.Request
Writer ResponseWriter
Params Params // 成员名 和 类型名 是一样的, 容易混淆
// ......
}
type Params []Param // 一个 Param类型的切片
type Param struct {
Key string // 路径参数的名, 比如: name
Value string // 给路径参数传递的值, 比如之前传递的: 夏色祭
}
但是我们调用的 context.Param 可不是上面这个结构体,而是一个方法,再说结构体也没法调用啊。
// 我们看到这个方法名也叫Param, 所以确实有点容易混淆
func (c *Context) Param(key string) string {
// 调用了c.Params.ByName(key), 至于逻辑, 不用想肯定是不断地循环每一个路径参数, 看看名字是否一样
return c.Params.ByName(key)
}
func (ps Params) ByName(name string) (va string) {
va, _ = ps.Get(name)
return
}
func (ps Params) Get(name string) (string, bool) {
// 循环, 如果 Key 和 指定的name相同, 那么返回对应的 value 和 true
for _, entry := range ps {
if entry.Key == name {
return entry.Value, true
}
}
// 否则返回空字符串 和 false
// 所以从这里也能看出, 如果指定的路径参数不存在, 那么得到就是一个空字符
return "", false
}
查询参数
Web提供的服务通常是client和server的交互。其中客户端向服务器发送请求,除了路径参数,其他的参数无非两种,查询字符串query string和报文体body参数。所谓query string,即路由中在路径之后用?连接的 key1=value2&key2=value2 的形式的参数。当然这个key-value会经过urlencode编码。
而查询字符串(查询参数)可以通过 DefaultQuery 或 Query 方法获取。而对于参数的处理,经常会出现参数不存在的情况,对于是否提供默认值,gin也考虑了,并且给出了一个优雅的方案。使用c.DefaultQuery方法读取参数,其中当参数不存在的时候,提供一个默认值;使用 Query 方法读取正常参数,当参数不存在的时候,返回空字串。
package main
import (
"github.com/gin-gonic/gin"
"log"
)
func main() {
router := gin.Default()
router.GET("/girl", func(context *gin.Context) {
// 不存在的话指定一个默认参数
name := context.DefaultQuery("name", "夏色祭")
// context.Query 等价于 context.Request.URL.Query().Get
age := context.Request.URL.Query().Get("age")
context.String(200, "姓名: %s; 年龄: %s", name, age)
})
if err := router.Run("localhost:22333"); err != nil {
log.Fatal(err)
}
}
我们来测试一下:
请求: http://localhost:22333/girl?age=16
返回: 姓名: 夏色祭; 年龄: 16
请求: http://localhost:22333/girl
返回: 姓名: 夏色祭; 年龄:
请求: http://localhost:22333/girl?name=神乐七奈
返回: 姓名: 神乐七奈; 年龄:
不过问题来了,查询参数是可以重复的,比如:
请求: http://localhost:22333/girl?name=神乐七奈&name=夏色祭
返回: 姓名: 神乐七奈; 年龄:
我们看到只返回了一个,如果希望都返回的话该怎么做呢?
package main
import (
"github.com/gin-gonic/gin"
"log"
)
func main() {
router := gin.Default()
router.GET("/girl", func(context *gin.Context) {
name := context.QueryArray("name") // 多个值组成一个列表
age := context.QueryArray("age") // 多个值组成一个列表
context.String(200, "姓名: %v; age: %v", name, age)
})
if err := router.Run("localhost:22333"); err != nil {
log.Fatal(err)
}
}
我们测试一下,看看返回结果。
请求: http://localhost:22333/girl?name=神乐七奈&name=夏色祭
返回: 姓名: [神乐七奈 夏色祭]; age: []
我们看到返回了都是切片,如果不存在相应的查询参数,则返回一个空切片。
表单参数
http的报文体传输数据就比query string稍微复杂一点,常见的格式就有四种。例如 application/json、application/x-www-form-urlencoded、application/xml 和 multipart/form-data,表单参数通过 PostForm 方法获取。
我们先定义一个模板文件吧。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登陆</title>
</head>
<body>
<form action="/login" method="post" enctype="application/x-www-form-urlencoded">
<table>
<tr>
<td>用户名: </td>
<td><input type="text" name="username"></td>
</tr>
<tr>
<td>密码: </td>
<td><input type="password" name="password"></td>
</tr>
<tr>
<td>夏色祭可爱吗: </td>
<td>
<input type="checkbox" name="hobby" value="girl">可爱
<input type="checkbox" name="hobby" value="girl">非常可爱
<input type="checkbox" name="hobby" value="girl">超级可爱
</td>
</tr>
</table>
</form>
</body>
</html>
然后我们使用 gin 来渲染这个模板,所以这个 Handler 要同时支持 GET 和 POST 请求。
package main
import (
"github.com/gin-gonic/gin"
"log"
)
func loginHandler (context *gin.Context) {
if context.Request.Method == "GET" {
// 调用context.HTML 渲染模板
// 状态码、模板名、参数( 用于渲染模板中的 {{}}, 这里我们没有使用模板语法, 所以传个 gin.H{} 即可 )
context.HTML(200, `login.html`, nil)
} else {
// 如果不存在的话, 得到的是空字符串, 但是我们也可以设置默认值, 和Query是类似的
username := context.DefaultPostForm("username", "夏色祭")
password := context.PostForm("password")
// 如果提交多个值, 我们可以使用PostFormArray获取
可愛い := context.PostFormArray("可愛い?") // 变量命名请用英文, 我这里只是单纯为了好玩
context.String(200, "姓名: %v; 密码: %v; 可愛い: %v", username, password, 可愛い)
}
}
func main() {
router := gin.Default()
// 这里很关键, 我们的 login.html 是写在当前目录的 templates 目录中的, 所以必须指定模板所在的目录
// templates/* 表示从templates目录中加载模板文件
router.LoadHTMLGlob("templates/*")
router.Any("/login", loginHandler)
if err := router.Run("localhost:22333"); err != nil {
log.Fatal(err)
}
}
下面访问一下:localhost:22333/login 看看结果。
还是很方便的,顺便提一句,gin 底层也是大量使用了 net/http 标准库的,只是使用起来更加的方便,下面总结一下:
-
路径参数: 形如/girl/:namecontext.Param("name")获取指定名称的路径参数的值;context.Params获取所以路径参数的值;
-
查询参数: 形如/girl?name=夏色祭&age=16context.DefaultQuery("name", "夏色祭"), 获取指定查询参数的值, 不存在的话使用默认值;context.Query("name"), 获取指定查询参数的值, 不存在的话返回空字符串;context.QueryArray("name"), 获取指定查询参数的值, 以列表形式返回, 因为可以指定多个同名的查询参数;
-
表单参数: 和查询参数类似context.DefaultPostForm("username", "夏色祭"), 获取指定表单参数的值, 不存在的话使用默认值;context.PostForm("username"), 获取指定表单参数的值, 不存在的话使用空字符串;context.PostFormArray("name"), 获取指定表单参数的值, 以列表形式返回, 因为可以指定多个同名的表单参数;
文件上传
前面介绍了基本的数据发送,其中 multipart/form-data 专用于文件上传。gin文件上传也很方便,和原生的net/http方法类似,不同在于gin把原生的Request封装到context.Request中了。
老规矩,先定义一个html文件:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登陆</title>
</head>
<body>
<form action="/upload" method="post" enctype="multipart/form-data">
<table>
<tr>
<td>用户名: </td>
<td><input type="text" name="username"></td>
</tr>
<tr>
<td>密码: </td>
<td><input type="password" name="password"></td>
</tr>
<tr>
<td>文件: </td>
<td>
<input type="file" name="file_upload">
</td>
</tr>
<tr>
<td>提交: <input type="submit" value="提交"></td>
</tr>
</table>
</form>
</body>
</html>
然后看看如何使用 gin 接收文件:
package main
import (
"github.com/gin-gonic/gin"
"io/ioutil"
"log"
)
func loginHandler (context *gin.Context) {
if context.Request.Method == "GET" {
context.HTML(200, "upload.html", nil)
} else {
username := context.DefaultPostForm("username", "夏色祭")
password := context.PostForm("password")
// 直接使用 FromFile 即可, 返回一个 FileHeader 和一个 error, 这个 FileHeader 我们在介绍 net/http 的时候说过
file, _ := context.FormFile("file_upload")
// _ = context.SaveUploadedFile(file, "新上传的: " + file.Filename) // 可以直接保存起来, 传递一个 *FileHeader 和 一个路径(string)
// 但是在介绍net/http的时候说过, 也可以调用 open 方法, 直接返回 io.Reader
f, _ := file.Open()
data, _ := ioutil.ReadAll(f)
context.String(200, "姓名: %v; \n密码: %v; \n文件内容: \n%v", username, password, string(data))
}
}
func main() {
router := gin.Default()
router.LoadHTMLGlob("templates/*")
router.Any("/upload", loginHandler)
if err := router.Run("localhost:22333"); err != nil {
log.Fatal(err)
}
}
所以我们看到还是很方便的,主要是 gin 把所有的参数都封装到 *gin.Context 中了,我们直接通过 context 就可以进行调用了。
但是注意,如果用户没有传递文件,那么得到的 file 就是个 nil。所以 FormFile 返回两个参数,第二个参数是一个 error,只不过这里我们省略了,但是生产环境中是需要进行处理的。
多文件上传
多文件上传也很简单,只是多写一个循环罢了。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登陆</title>
</head>
<body>
<form action="/upload" method="post" enctype="multipart/form-data">
<table>
<tr>
<td>用户名: </td>
<td><input type="text" name="username"></td>
</tr>
<tr>
<td>密码: </td>
<td><input type="password" name="password"></td>
</tr>
<tr>
<td>文件: </td>
<td>
<input type="file" name="file_upload" multiple>
</td>
</tr>
<tr>
<td>提交: <input type="submit" value="提交"></td>
</tr>
</table>
</form>
</body>
</html>
我们看看如何使用 gin 来接收多个文件:
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"io/ioutil"
"log"
"strings"
)
func loginHandler (context *gin.Context) {
if context.Request.Method == "GET" {
context.HTML(200, `upload.html`,nil)
} else {
buf := strings.Builder{}
username := context.DefaultPostForm("username", "夏色祭")
password := context.PostForm("password")
buf.WriteString(fmt.Sprintf("姓名: %v;\n\n密码: %v;\n\n", username, password))
// 这里和net/http是类似的, 返回的 form 是一个 *multipart.Form
// 这是一个结构体里面有两个成员, 分别是 Value map[string][]string 和 File map[string][]*FileHeader
// Value用于获取表单内容, File用于获取文件内容; 但是我们只获取文件, 表单内容依旧可以通过 context 直接获取
form, err := context.MultipartForm()
if err != nil {
context.String(404, err.Error())
return
}
// 获取多个文件, 会返回一个 []*FileHeader
files := form.File["file_upload"]
// 直接循环遍历即可
for _, file := range files {
// 可以直接保存, 这里我们返回给客户端
if f, err := file.Open(); err != nil {
context.String(404, err.Error())
} else {
data, _ := ioutil.ReadAll(f)
buf.WriteString(fmt.Sprintf("文件名: %v, 文件内容:\n%v\n\n", file.Filename, string(data)))
}
}
context.String(200, buf.String())
}
}
func main() {
router := gin.Default()
router.LoadHTMLGlob("templates/*")
router.MaxMultipartMemory = 8 << 20 // 限制上传的文件大小
router.Any("/upload", loginHandler)
if err := router.Run("localhost:22333"); err != nil {
log.Fatal(err)
}
}
下面就来上传文件,我们将之前的文件再拷贝两份之后上传吧。
路由组
router group是为了方便管理一部分相同的URL,举个栗子:
/shop/index
/shop/cart
/shop/order
/shop/rank
/book/index
/book/cart
/book/order
/book/rank
上面这种路由明显是可以进行分类的,shop分为一组,book分为一组。而 gin 也支持我们这么做:
package main
import (
"github.com/gin-gonic/gin"
"log"
)
func main() {
router := gin.Default()
// 定义一个组, 然后使用这个组注册路由
v1 := router.Group("/v1")
// 这个大括号有没有均可, 只是为了美观
{
// 以后在访问的时候要加上对应的前缀, 比如: v1/login
v1.GET("/login", func(context *gin.Context) {
context.String(200, "v1: login")
})
v1.GET("/access", func(context *gin.Context) {
context.String(200, "v1: access")
})
}
v2 := router.Group("/v2")
{
v2.GET("/login", func(context *gin.Context) {
context.String(200, "v2: login")
})
v2.GET("/access", func(context *gin.Context) {
context.String(200, "v2: access")
})
}
if err := router.Run("localhost:22333"); err != nil {
log.Fatal(err)
}
}
比较简单,相信不需要解释,我们演示一下:
请求: http://localhost:22333/v1/login
返回: v1: login
请求: http://localhost:22333/v1/access
返回: v1: access
请求: http://localhost:22333/v2/login
返回: v2: login
请求: http://localhost:22333/v2/access
返回: v2: access
当然组之间也是可以层层嵌套的,举个栗子:
package main
import (
"github.com/gin-gonic/gin"
"log"
)
func main() {
router := gin.Default()
// 定义一个组, 然后使用这个组注册路由
v1 := router.Group("/v1")
{
v1.GET("/login", func(context *gin.Context) {
context.String(200, "v1: login")
})
v1.GET("/access", func(context *gin.Context) {
context.String(200, "v1: access")
})
// 嵌套路由组
v2 := v1.Group("/v2")
v2.GET("/login", func(context *gin.Context) {
context.String(200, "v1/v2: login")
})
v2.GET("/access", func(context *gin.Context) {
context.String(200, "v1/v2: access")
})
}
if err := router.Run("localhost:22333"); err != nil {
log.Fatal(err)
}
}
相信你已经知道结果了。
请求: http://localhost:22333/v1/login
返回: v1: login
请求: http://localhost:22333/v1/access
返回: v1: access
请求: http://localhost:22333/v1/v2/login
返回: v1/v2: login
请求: http://localhost:22333/v1/v2/access
返回: v1/v2: access
因此路由组最明显的好处就是分类,在划分业务逻辑和API版本的时候推荐使用路由组。
映射请求数据
映射请求数据指的是什么呢?首先我们在解析数据的时候是不是一个字段一个字段解析的呢,比如:username解析一下、password解析一下,如果字段非常多的话是不是很麻烦呢?而 gin 已经帮我们想好了,那么下面就来看看它是怎么做的吧。
数据解析绑定
顾名思义,就是将数据绑定给一个类型,然后根据用户传递的数据解析到指定的结构体变量中(类似于序列化和反序列化)。
package main
import (
"github.com/gin-gonic/gin"
"log"
)
type Girl struct {
// 这里字段一定要大写, 至于``里面的内容一会单独说
Username string `form:"username1" json:"username2"`
Password string `form:"password1" json:"password2"`
}
func loginHandler(context *gin.Context) {
var g Girl
// 将数据和结构体对象g绑定在一起, 这样传递数据时就会自动赋值给g里面对应的成员
err := context.ShouldBind(&g) // 一定要传递指针, 因为Go是值传递
if err != nil {
context.String(404, "解析错误")
return
}
context.String(200, "username: %v, password: %v", g.Username, g.Password)
}
func main() {
router := gin.Default()
router.POST("/login", loginHandler)
if err := router.Run("localhost:22333"); err != nil {
log.Fatal(err)
}
}
这里我们解释一下结构体中的:form:"username1" json:"username2",因为绑定的数据可以来自于表单(application/x-www-form-urlencoded),也可以来自于json(application/json)。如果是通过表单,那么表单中name="username1"的值会给结构体中Username成员,name="password1"的值会给结构体中Password成员;如果是通过json提交,那么json中key为"username2"对应的value会给Username成员,key为"password2"对应的成员会给Password成员。
所以``当中的form和json应该都是同一个名字,只不过我们这里为了演示用法可以用的不同的名字。
下面我们使用Python来测试一下,当然你也可以使用Postman。
但是问题来了,gin 是怎么做到的呢?因为我们提交的时候会有一个 Content-Type,gin 会根据请求类型判断解析的时候究竟该用哪种方式解析。
另外如果解析失败的话,就会报错,举个栗子:
type Girl struct {
// 一般form和json起的都是相同的名字, 因为无论使用表单提交、还是使用json提交, 参数名应该都是一样的
// 不存在表单提交使用 username1、password1, json提交使用 username2、password2
Username string `form:"username" json:"username"`
Password int `form:"password" json:"password"` // 这里我们将 string 改成 int
}
func loginHandler(context *gin.Context) {
var g Girl
err := context.ShouldBind(&g) // 一定要传递指针, 因为Go是值传递
if err != nil {
context.String(404, "解析错误, 错误原因: %v", err.Error())
return
}
context.String(200, "username: %v, password: %v", g.Username, g.Password)
}
我们来测试一下:
除了ShouldBind之外,还有BindJSON,默认按照json格式解析;以及Bind,默认按照form表单格式进行解析,它们的用法都是一样的。
注意:使用绑定方法时,gin 会根据请求头中的 Content-Type 来自动推断需要解析的类型。另外,如果没有传递相应的值,那么绑定字段会得到零值,如果你希望这种情况下不给零值、而是报错的话,那么可以在结构体成员标签中加上 binding:"required",此时如果值为空,那么会请求失败并返回错误。
package main
import (
"github.com/gin-gonic/gin"
"log"
)
type Girl struct {
// 要求字段必须传递
Username string `form:"username" json:"username" binding:"required"`
Password string `form:"password" json:"password" binding:"required"`
}
func loginHandler(context *gin.Context) {
var g Girl
err := context.ShouldBind(&g)
if err != nil {
context.String(404, "解析错误, 错误原因: %v", err.Error())
return
}
context.String(200, "username: %v, password: %v", g.Username, g.Password)
}
func main() {
router := gin.Default()
router.POST("/login", loginHandler)
if err := router.Run("localhost:22333"); err != nil {
log.Fatal(err)
}
}
所以如果希望只支持 form 或者 json 的话,那么就将标签中对应的 form 或者 json去掉,然后加上 binding:"required" 即可,这样的话就必须传递而且通过指定的方式进行传递。
URL绑定
下面来看看如何在GET请求中,如何和URL中的路径参数进行绑定:
package main
import (
"github.com/gin-gonic/gin"
"log"
)
type Girl struct {
// 路径参数的话, 标签使用uri, 当然这里的 form 和 json可以删掉了, 但是写上也不碍事
Username string `form:"username" json:"username" uri:"username"`
Password string `form:"password" json:"password" uri:"password"`
}
func loginHandler(context *gin.Context) {
var g Girl
// 将路径参数的值绑定到结构体上面, 这里使用ShouldBindUri
err := context.ShouldBindUri(&g)
if err != nil {
context.String(404, "解析错误, 错误原因: %v", err.Error())
return
}
// 通过context.Param进行获取
username := context.Param("username")
password := context.Param("password")
// 然后进行显示, 结果肯定是一样的
context.String(200, "username: %v, password: %v\n", username, password)
context.String(200, "username: %v, password: %v", g.Username, g.Password)
}
func main() {
router := gin.Default()
// 指定路径参数
router.GET("/login/:username/:password", loginHandler)
if err := router.Run("localhost:22333"); err != nil {
log.Fatal(err)
}
}
然后请求一下试试:
这里的结构体中的字段可以不需要 binding:"required" 了,因为这是GET请求、而且是路径参数,如果不传递的话压根就访问不到这个路由。
当然啦,form表单有了、json也有了,路径参数也有啦,那么怎么能少了查询参数呢?
package main
import (
"github.com/gin-gonic/gin"
"log"
)
type Girl struct {
// 令人费解的是, 查询参数对应的tag也是使用 query
Username string `form:"username"`
Password string `form:"password"`
}
func loginHandler(context *gin.Context) {
var g Girl
// 将查询参数的值绑定到结构体上面, 这里使用ShouldBindQuery
err := context.ShouldBindQuery(&g)
if err != nil {
context.String(404, "解析错误, 错误原因: %v", err.Error())
return
}
username := context.Query("username")
password := context.Query("password")
context.String(200, "username: %v, password: %v\n", username, password)
context.String(200, "username: %v, password: %v", g.Username, g.Password)
}
func main() {
router := gin.Default()
router.GET("/login", loginHandler)
if err := router.Run("localhost:22333"); err != nil {
log.Fatal(err)
}
}
还没完,除了上述之外,gin 还支持请求头的数据绑定,我们来看一下:
package main
import (
"github.com/gin-gonic/gin"
"log"
)
type Girl struct {
Username string `header:"username"`
Password string `header:"password"`
}
func loginHandler(context *gin.Context) {
var g Girl
// 绑定请求头的话使用ShouldBindHeader
err := context.ShouldBindHeader(&g)
if err != nil {
context.String(404, "解析错误, 错误原因: %v", err.Error())
return
}
// 等价于 context.Request.Header.Get()
username := context.GetHeader("username")
password := context.GetHeader("password")
context.String(200, "username: %v, password: %v\n", username, password)
context.String(200, "username: %v, password: %v", g.Username, g.Password)
}
func main() {
router := gin.Default()
router.GET("/login", loginHandler)
if err := router.Run("localhost:22333"); err != nil {
log.Fatal(err)
}
}
常见的数据绑定就是以上几种,我们总结一下:
ShouldBind: 用于绑定json和form, 结构体字段的标签也是用json和form;ShouldBindJson: 用于绑定json;ShouldBindUri: 用于绑定uri, 结构体字段的标签使用uri;ShouldBindQuery: 用于绑定查询字段, 结构体字段的标签使用form;ShouldBindHeader: 用于绑定请求头, 结构体字段的标签使用header;
以上这些统统可以使用ShouldBindWith:
context.ShouldBindWith(&g, binding.JSON)
context.ShouldBindWith(&g, binding.Header)
//.......
但是一般我们很少使用这种方式。
gin 响应数据
响应数据我们已经见识过了,像 context.String、context.JSON、context.HTML等等;调用的方法不同,那么响应头也是不一样的,比如:
如果是 context.String: 那么响应头中的 Content-Type 是 text/plain; charset=utf-8;如果是 context.JSON: 那么响应体头的 Content-Type 是 application/json; charset=utf-8;如果是 context.HTML: 那么响应体头的 Content-Type 是 text/html; charset=utf-8;
当然还有 context.XML、context.YAML、context.ProtoBuf 等等,这里我们主要介绍如何设置返回的响应头,正如请求一样,我们在返回数据的时候也可以进行设置一些头部信息,并且 gin 也提供了很优雅的方法。
package main
import (
"github.com/gin-gonic/gin"
"log"
)
func main() {
router := gin.Default()
router.GET("/test", func(context *gin.Context) {
// 等价于 context.Writer.Header().Set, 要是获取头部信息的话直接使用 context.GetHeader 即可
context.Header("hello", "matsuri")
context.Header("ping", "pong")
})
if err := router.Run("localhost:22333"); err != nil {
log.Fatal(err)
}
}
这里自动将key大写了,当然这不奇怪,因为浏览器也是这样的,我们看一下:
package main
import (
"github.com/gin-gonic/gin"
"log"
)
func main() {
router := gin.Default()
router.GET("/test", func(context *gin.Context) {
// 设置 Content-Type
context.Header("Content-Type", "xxx")
context.Header("ping", "pong")
})
if err := router.Run("localhost:22333"); err != nil {
log.Fatal(err)
}
}
当然啦我们还可以设置 cookie,context.Cookie 表示获取cookie,context.SetCookie表示设置cookie,可以自己试一下。
另外之前对了忘记说了,还有一个重定向,gin 里面如何实现重定向呢?我们来看一下:
package main
import (
"github.com/gin-gonic/gin"
"log"
)
func main() {
router := gin.Default()
router.GET("/test", func(context *gin.Context) {
context.Redirect(302, "http://www.baidu.com")
})
if err := router.Run("localhost:22333"); err != nil {
log.Fatal(err)
}
}
当我们访问 /test 的时候,就会跳转到百度页面。当然,gin 还支持我们神不知鬼不觉地跳转。
package main
import (
"github.com/gin-gonic/gin"
"log"
)
func main() {
router := gin.Default()
router.GET("/test", func(context *gin.Context) {
// 将路由换成 /test2
context.Request.URL.Path = "/test2"
// 直接执行 /test2 对应的处理函数
router.HandleContext(context)
})
router.GET("/test2", func(context *gin.Context) {
context.String(200, "这是 /test2")
})
if err := router.Run("localhost:22333"); err != nil {
log.Fatal(err)
}
}
最后,我们还可以异步返回。比如:有一个处理函数,需要连接数据库执行复杂的任务,但是我们又不希望等到任务结束之后才返回。那么这个时候,就可以将任务异步执行。
package main
import (
"github.com/gin-gonic/gin"
"log"
"time"
)
func main() {
router := gin.Default()
router.GET("/test", func(context *gin.Context) {
go func() {
// 由于 context 是贯穿整个请求的, 所以最好不让多个goroutine操作同一个context
// 因此我们需要拷贝一份, 当然获取查询参数其实直接在外面获取就行
c := context.Copy()
query := c.Query("query")
time.Sleep(time.Second * 2) // 模拟任务执行
// 注册一个路由
router.GET("/test2", func(context *gin.Context) {
context.String(200, "%s 已经执行完毕", query)
})
}()
context.String(200, "任务已提交, 请稍后通过 /test2 查看")
})
if err := router.Run("localhost:22333"); err != nil {
log.Fatal(err)
}
}
所以我们发现 go 里面的协程真的是非常方便,goroutine 确实是 go 的一个大杀器。
文件响应
这里我们再看看如何在 gin 中返回一个文件,我们之前都是将文件的内容读取出来之后返回的。另外,gin 里面还有关于模板方面的知识,这里我们就不说了,因为前后端分离现在是主流,后端只需要返回相应的json数据即可,前端拿到数据直接渲染。
如果还使用传统的mvc,那么一个模板文件里面必定会混有前端和后端逻辑,这就要求前端人员懂后端模板渲染语法,或者要求后端人员懂的前端语法。所以前后端分离之后就解决了这一问题,避免了前端和后端的直接交互。
当然我这里并不是说这一个项目的时候就一定要使用前后端分离,具体看自身的业务,只是我这里偷懒想找一个借口,因此 gin 渲染模板方面的知识这里就不说了。
func main() {
router := gin.Default()
// 显示指定目录下的文件
router.StaticFS("/show_dir", http.Dir(`C:\Users\satori\go\src\github.com\gin-gonic\gin\render`))
if err := router.Run("localhost:22333"); err != nil {
log.Fatal(err)
}
}
点击的话,还可以查看具体文件的内容。当然啦,还可以返回一个具体的文件。
func main() {
router := gin.Default()
router.StaticFile("/image", "./kww7z6.jpg")
if err := router.Run("localhost:22333"); err != nil {
log.Fatal(err)
}
}
但是这两种方式都不是很完美,因为StaticFS将整个目录的内容都返回了,而StaticFile一次只能返回一个文件。我们希望的是通过路径参数,来返回指定的文件,比如:/image/1.png,那么就返回 1.png。但是很遗憾,StaticFile不支持路径参数,因此只能回到传统的方式。
func main() {
router := gin.Default()
router.GET("/image/:file", func(context *gin.Context) {
// 获取文件名
filename := context.Param("file")
// 设置请求头中的 Content-Disposition
context.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename))
// 读取文件内容
data, _ := ioutil.ReadFile(filename)
// 返回, 调用 context.Data 方法
context.Data(200, "application/x-gzip", data)
})
if err := router.Run("localhost:22333"); err != nil {
log.Fatal(err)
}
}
此时即可指定访问的资源,会自动下载到本地。
gin 中间件
Go 的标准库 net/http 设计的一大特点就是特别容易构建中间件,而 gin 也提供为了类似的中间件。需要注意的是,中间件只对注册过的路由函数起作用,对于分组路由,嵌套使用中间件,可以限定中间件的作用范围。中间件分为 全局中间件、单个路由中间件、群组中间件。
我们之前说过,context 是 gin 的核心,它的构造如下:
// Context is the most important part of gin. It allows us to pass variables between middleware,
// manage the flow, validate the JSON of a request and render a JSON response for example.
type Context struct {
writermem responseWriter
Request *http.Request
Writer ResponseWriter
Params Params
handlers HandlersChain
index int8
fullPath string
engine *Engine
params *Params
// This mutex protect Keys map
mu sync.RWMutex
// Keys is a key/value pair exclusively for the context of each request.
Keys map[string]interface{}
// Errors is a list of errors attached to all the handlers/middlewares who used this context.
Errors errorMsgs
// Accepted defines a list of manually accepted formats for content negotiation.
Accepted []string
// queryCache use url.ParseQuery cached the param query result from c.Request.URL.Query()
queryCache url.Values
// formCache use url.ParseQuery cached PostForm contains the parsed form data from POST, PATCH,
// or PUT body parameters.
formCache url.Values
// SameSite allows a server to define a cookie attribute making it impossible for
// the browser to send this cookie along with cross-site requests.
sameSite http.SameSite
}
看一下里面的成员 handlers HandlersChain,通过源码我们知道 HandlersChain 不过是 []HandlerFunc 的别名。
type HandlersChain []HandlerFunc
// HandlerFunc defines the handler used by gin middleware as return value.
type HandlerFunc func(*Context) // 而HandlerFunc是接收一个 *gin.Context 、没有返回值的函数
所以中间件和普通的处理函数、或者说路由函数之间没有任何区别,我们怎么写 HandlerFunc 就可以怎么写中间件。
说了这么多,那么什么是中间件呢?
中间件,英文名是 middleware,如果你了解 Python 的话,你可以简单地认为 中间件 就类似于 Python 里面的装饰器。如果每一个请求过来在处理的时候,都要经过相同的一步或多步,那么我们便可以将这一步或多步以中间件的形式抽离出来。
比如:如果你用过 flask,那么
@login_required估计你再熟悉不过了,有些请求必须是登陆之后才可以进行的。那么便可以针对需要登陆的请求设置一个中间件,让那些所有需要登陆的请求都经过这个中间件,而不用每一个处理函数都写一个登陆验证逻辑。
所以我们看到中间件的一个好处就是可以减少代码量,使逻辑更加清晰。那么下面来看看,如何在 gin 里面使用中间件。
首先是全局中间件,注册中间件的方式使用 router.Use,我们在创建router的时候,是使用 gin.Default(),这里面便使用了中间件,我们看一下。
// gin 里面叫做 engine, 不过更习惯将其叫成 router
func Default() *Engine {
debugPrintWARNINGDefault()
engine := New() // 创建一个engine
// 调用Use方法, 我们看到这里注册了两个中间件, 一个是 Logger() 用于打印日志, 还有一个是 Recovery()
// 因为我们在启动服务的时候, 请求到来的时候, 控制台会出现大量的日志信息
// 以及某个处理函数出错的时候, 并没有挂掉, 这些都是中间件在背后"捣的鬼"(做的幕后工作)
engine.Use(Logger(), Recovery())
return engine
}
// 而Use方法也很简单, 接受多个HandlerFunc, 这个HandlerFunc我们之前见过, 就是一个 func(*Context)
// 它和 GET、POST等请求里面的处理函数是一样的
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
engine.RouterGroup.Use(middleware...)
engine.rebuild404Handlers()
engine.rebuild405Handlers()
return engine
}
下面我们来进行编码:
package main
import (
"github.com/gin-gonic/gin"
"log"
)
// 定义两个中间件
func middleware1(context *gin.Context) {
context.String(200, "middleware1\n")
}
func middleware2(context *gin.Context) {
context.String(200, "middleware2\n")
}
func main() {
router := gin.Default()
// 注册两个中间件, 此时注册的是全局中间件
router.Use(middleware1, middleware2)
router.GET("/test", func(context *gin.Context) {
context.String(200, "test")
})
if err := router.Run("localhost:22333"); err != nil {
log.Fatal(err)
}
}
此时在访问的时候,会先走两个中间件,然后再执行注册的处理函数。并且所有的内容都会返回给客户端,注意:我们之前一直没有说,如果在两个 context.String之间加上一个 time.Sleep 会怎么样。由于 HTTP 协议是无状态的,一次请求、一次返回,所以即便存在 time.Sleep,也会将整个函数逻辑执行完之后将内容一次性返回给客户端。另外我们看到,指定一个不存在的路由本来应该是返回 404 page not found,但是由于定义了全局中间件,所以即使访问不存在的路由,也会执行中间件。
package main
import (
"github.com/gin-gonic/gin"
"log"
)
func middleware1(context *gin.Context) {
context.String(200, "middleware1\n")
}
func middleware2(context *gin.Context) {
context.String(200, "middleware2\n")
}
func main() {
router := gin.Default()
router.Use(middleware1, middleware2)
router.NoRoute(func(context *gin.Context) {
context.String(404, "页面去火星了。。。。")
})
if err := router.Run("localhost:22333"); err != nil {
log.Fatal(err)
}
}
全局中间件说完了,那么如何注册局部中间件呢?很简单,我们看一下GET、POST这些方法。
// POST is a shortcut for router.Handle("POST", path, handle).
func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle(http.MethodPost, relativePath, handlers)
}
// GET is a shortcut for router.Handle("GET", path, handle).
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle(http.MethodGet, relativePath, handlers)
}
这些方法都是可以接收多个 HandlerFunc,所以很明显,就在这里面注册即可。
package main
import (
"github.com/gin-gonic/gin"
"log"
)
func middleware1(context *gin.Context) {
context.String(200, "middleware1\n")
}
func middleware2(context *gin.Context) {
context.String(200, "middleware2\n")
}
func main() {
router := gin.Default()
// 这里注册的是全局中间件, 任何请求都会执行这两个中间件
router.Use(middleware1, middleware2)
// 局部中间件
router.GET("/test", middleware1, func(context *gin.Context) {
context.String(200, "xxxxxx\n")
}, middleware2)
if err := router.Run("localhost:22333"); err != nil {
log.Fatal(err)
}
}
因此正如之前说的,中间件和处理函数本质上没有任何区别,一个路由可以对应多个处理函数。如果有一个处理函数是所有请求都必须经过的,那么就可以将其注册成全局中间件的形式。
此外,中间件之间可以传递变量。通过 context.Set 设置变量,context.Get 获取变量。
func main() {
router := gin.Default()
router.GET("/test", func(context *gin.Context) {
// 设置, Set方法接收 一个 string 和 一个 interface{}
context.Set("ping", "pong")
}, func(context *gin.Context) {
// 获取, 然后返回 interface{} 和 error, 可以使用断言判断返回值类型, 不过这里我们就直接返回了
s, _ := context.Get("ping")
context.String(200, "%v", s)
})
if err := router.Run("localhost:22333"); err != nil {
log.Fatal(err)
}
}
路由组也是可以进行中间件注册的,我们来看一下:
package main
import (
"github.com/gin-gonic/gin"
"log"
)
func middleware1(context *gin.Context) {
context.String(200, "middleware1\n")
}
func middleware2(context *gin.Context) {
context.String(200, "middleware2\n")
}
func main() {
router := gin.Default()
v1_group := router.Group("/v1")
v1_group.Use(middleware1, middleware2)
/*
或者直接使用
v1_group := router.Group("/v1", middleware1, middleware2)
也是可以的, 效果一样
*/
{
v1_group.GET("/test", func(context *gin.Context) {
context.String(200, "xxxxxx\n")
})
}
if err := router.Run("localhost:22333"); err != nil {
log.Fatal(err)
}
}
此时只有 /v1 开头的路由过来才会走这两个中间件。
但是很多时候,我们都会讲中间件写成闭包的形式,比如:
package main
import (
"github.com/gin-gonic/gin"
"log"
)
func login_required (ok bool) func (context *gin.Context) {
return func(context *gin.Context) {
if ok {
// 需要登录验证, 必须通过查询参数传递 username 和 password
_, ok1 := context.GetQuery("username")
_, ok2 := context.GetQuery("password")
// context.Query("username") 底层调用了 context.GetQuery("username")
// context.GetQuery("username") 会返回一个 string 和 bool, 我们可以根据这个bool来判断用户是否传递了指定的查询参数
if !ok1 || !ok2 {
context.String(404, "请传递查询参数 username 和 password")
// 我们知道一个路由可以对应多个处理函数(中间件), 当然还有全局中间件
// 它们会像链子一样串起来, 一个执行完了执行下一个
// 但是程序走到这里我们显然不希望再执行后面的处理函数, 而是让用户指定查询参数重新发起请求
// 于是调用 context.Abort(), 表示就此结束, 不再执行后面的 HandlerFunc
context.Abort()
}
}
}
}
func main() {
router := gin.Default()
router.GET("/access1", login_required(true), func(context *gin.Context) {
context.String(200, "/access1")
})
router.GET("/access2", login_required(false), func(context *gin.Context) {
context.String(200, "/access2")
})
if err := router.Run("localhost:22333"); err != nil {
log.Fatal(err)
}
}
所以我们便实现了相关的登录控制,/access1 要求传递相关查询参数,所以是 login_required(true);但是 /access2 不需要,所以是 login_required(false),但是事实上,这里也不需要这个中间件,当然如果还有其它逻辑的话就另当别论了。
context.Next
首先我们知道 context.Abort() 是中止后续 HandlerFunc 的执行,而除了 context.Abort之外还有一个 context.Next。从名字上看,context.Next 是直接执行下一个 HandlerFunc。
举个栗子:
package main
import (
"github.com/gin-gonic/gin"
"log"
)
func middleware1(context *gin.Context) {
context.String(200, "middleware1\n")
}
func middleware2(context *gin.Context) {
// 如果是 /test1, 那么执行 Next()
if context.Request.URL.Path == "/test1" {
context.Next()
}
context.String(200, "middleware2\n")
}
func middleware3(context *gin.Context) {
// 如果是 /test2, 那么执行 Next()
if context.Request.URL.Path == "/test2" {
context.Next()
}
context.String(200, "middleware3\n")
}
func middleware4(context *gin.Context) {
context.String(200, "middleware4\n")
}
func main() {
router := gin.Default()
router.GET("/test1", middleware1, middleware2, middleware3, middleware4, func(context *gin.Context) {
context.String(200, "/test1\n")
})
router.GET("/test2", middleware1, middleware2, middleware3, middleware4, func(context *gin.Context) {
context.String(200, "/test2\n")
})
if err := router.Run("localhost:22333"); err != nil {
log.Fatal(err)
}
}
我们来逐步分析,首先是 /test1,在经过中间件 middleware1 的时候,会返回 middleware1,很简单;但是在经过中间件 middleware2 的时候,调用了 context.Next(),这里会直接去执行 middleware3,然后是 middleware4,最后是处理函数本身。当最后一个 HandlerFunc 执行完毕之后,再将 middleware2 剩余的代码执行完毕即可。当然,对于 /test2 也是同理。
那么问题来了,这背后的机制是怎样的呢?首先多个 HandlerFunc 会被放在一个切片里,每个 HandlerFunc 都会有一个索引,当执行 context.Next 的时候,会直接将 index++,然后取出对应的 HandlerFunc,进行执行。我们通过源码来更好地理解这一过程:
func (c *Context) Next() {
// 直接将 index++
c.index++
// 这里转成了int8, 说明每个路由对应的处理函数的数量是有限的
for c.index < int8(len(c.handlers)) {
// 选择下一个HandlerFunc, 传入*Context进行执行
c.handlers[c.index](c)
// index++, 继续循环, 直到将最后一个 HandlerFunc 执行完毕
c.index++
}
}
理解完 context.Next() 之后,那么 context.Abort() 就很好解释了,显然是直接将索引设置为最大值即可。
func (c *Context) Abort() {
// 将整个 HandlerFunc 链条中断掉
c.index = abortIndex
}
// 一个路由对应的 HandlerFunc 的最大数量是 63 个
const abortIndex int8 = math.MaxInt8 / 2
所以如果需要执行多个Next,那么执行顺序很容易观察。
package main
import (
"github.com/gin-gonic/gin"
"log"
)
func middleware1(context *gin.Context) {
context.String(200, "middleware1\n")
}
func middleware2(context *gin.Context) {
context.Next()
context.String(200, "middleware2\n")
}
func middleware3(context *gin.Context) {
context.Next()
context.String(200, "middleware3\n")
}
func middleware4(context *gin.Context) {
context.String(200, "middleware4\n")
}
func main() {
router := gin.Default()
router.GET("/test1", middleware1, middleware2, middleware3, middleware4, func(context *gin.Context) {
context.String(200, "/test1\n")
})
if err := router.Run("localhost:22333"); err != nil {
log.Fatal(err)
}
}
执行到 middleware2,跳转到 middleware3,然后跳转到 middleware4;然后转过头执行 middleware3 剩余的代码,然后再执行 middleware2 剩余的代码。
如果是 Next 和 Abort 混合,那么也依旧不难。
package main
import (
"github.com/gin-gonic/gin"
"log"
)
func middleware1(context *gin.Context) {
context.String(200, "middleware1\n")
}
func middleware2(context *gin.Context) {
context.Next()
context.String(200, "middleware2\n")
}
func middleware3(context *gin.Context) {
context.Abort()
context.String(200, "middleware3\n")
}
func middleware4(context *gin.Context) {
context.String(200, "middleware4\n")
}
func main() {
router := gin.Default()
router.GET("/test1", middleware1, middleware2, middleware3, middleware4, func(context *gin.Context) {
context.String(200, "/test1\n")
})
if err := router.Run("localhost:22333"); err != nil {
log.Fatal(err)
}
}
执行到 middleware3 的时候出现了Abort,改变index;当 middleware3 执行完毕的时候,由于index的改变,导致 middleware4 和处理函数不会被执行;然后回到 middleware2,执行完剩余部分代码,整个请求结束。
所以 *gin.Context 是gin里面最重要的部分,它贯穿了整个请求。
所以 *gin.Context 是gin里面最重要的部分,它贯穿了整个请求。
BasicAuth简单认证
关于认证,gin 也提供了相应的策略,而且使用起来非常简单。
package main
import (
"github.com/gin-gonic/gin"
"log"
"net/http"
)
func main() {
router := gin.Default()
// 定义一个路由组, 同时绑定一个中间件, 里面是用户名和密码
authorized := router.Group("/admin", gin.BasicAuth(gin.Accounts{
"夏色祭": "123456",
"雫_lulu": "123456",
"绯赤艾利欧": "123456",
"神乐七奈": "123456",
}))
authorized.GET("/secrets", func(c *gin.Context) {
// 访问的时候会弹出输入框, 提示输入用户名和密码
// 如果用户名和密码输入正确, 那么 user 就是返回的用户名, 否则的话为 nil
user, _ := c.Get(gin.AuthUserKey)
if user != nil {
c.JSON(http.StatusOK, gin.H{"user": user})
} else {
c.JSON(http.StatusOK, gin.H{"user": user})
}
})
if err := router.Run(":22333"); err != nil {
log.Fatal(err)
}
}
认证失败的话,会让你重新填写;认证成功的话,会返回内容。
gin 路由原理
gin 的路由底层使用的是 httprouter,但是做了一些简单的修改,掌握了 httprouter,那么了解 gin 的路由实现也就不难了。
首先路由其实就相当于字符串匹配,路由器依赖于一种叫做前缀树的结构,这个有兴趣可以自己了解一下。
总结
总的来说,gin 是一个非常优秀的框架,在 GitHub 上所有 Go 的 Web 框架中,它的 Star 数是最高的。而且使用起来也比较简单,当然性能也是非常优秀的。琴酒,你好特么快啊。