Gin框架简介
框架一直是敏捷开发中的利器,能让开发者很快的上手并做出应用,甚至有的时候,脱离了框架,一些开发者都不会写程序了。成长总不会一蹴而就,从写出程序获取成就感,再到精通框架,快速构造应用,当这些方面都得心应手的时候,可以尝试改造一些框架,或是自己创造一个。
曾经我以为Python世界里的框架已经够多了,后来发现相比golang简直小巫见大巫。golang提供的net/http库已经很好了,对于http的协议的实现非常好,基于此再造框架,也不会是难事,因此生态中出现了很多框架。既然构造框架的门槛变低了,那么低门槛同样也会带来质量参差不齐的框架。
考察了几个框架,通过其github的活跃度,维护的team,以及生产环境中的使用率。发现Gin还是一个可以学习的轻巧框架。
Gin是一个golang的微框架,封装比较优雅,API友好,源码注释比较明确,已经发布了1.0版本。具有快速灵活,容错方便等特点。其实对于golang而言,web框架的依赖要远比Python,Java之类的要小。自身的net/http足够简单,性能也非常不错。框架更像是一些常用函数或者工具的集合。借助框架开发,不仅可以省去很多常用的封装带来的时间,也有助于团队的编码风格和形成规范。
Gin 包括以下几个主要的部分:
- 设计精巧的路由/中间件系统;
- 简单好用的核心上下文
Context
; - 附赠工具集(JSON/XML 响应, 数据绑定与校验等).
// 安装
go get -u github.com/gin-gonic/gin
// 使用的时候要导入包
import "github.com/gin-gonic/gin"
现在,我们可以简单写个HelloWorld了。
首先创建一个项目:gindemo,然后新建一个go文件(helloworld.go):
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.GET("/", func(c *gin.Context) {
c.String(http.StatusOK, "Hello, World!")
})
router.Run(":8000")
}
使用Gin实现Hello world非常简单,创建一个router(路由),然后执行Run方法即可。
右键运行项目,打开浏览器并访问指定的端口:http://127.0.0.1:8000/能看到游览器输出了Hello, World!
。访问后同时在终端能看到各种状态码的返回。或者我们通过终端的curl命令,也可以访问。
# 输入
curl http://127.0.0.1:8000
# 输出
Hello, World!
下面来仔细分析下上面的代码结构:
- 1、
router:=gin.Default()
:这是默认的服务器。使用gin的Default
方法创建一个路由Handler
; - 2、然后通过Http方法绑定路由规则和路由函数。不同于
net/http
库的路由函数,gin进行了封装,把request
和response
都封装到了gin.Context
的上下文环境中。 - 3、最后启动路由的Run方法监听端口。还可以用
http.ListenAndServe(":8080", router)
,或者自定义Http服务器配置。
要知道一次请求处理的大体流程,只要找到web框架的入口即可。通过我们上面的例子我们可以看到,Run方法十分耀眼,点击去可以看到关键的http.ListenAndServe,这意味着Engine这个结构体,实现了ServeHTTP这个接口。入口就是Engine实现的ServeHTTP接口。
// Run attaches the router to a http.Server and starts listening and serving HTTP requests.
// It is a shortcut for http.ListenAndServe(addr, router)
// Note: this method will block the calling goroutine indefinitely unless an error happens.
func (engine *Engine) Run(addr ...string) (err error) {
defer func() { debugPrintError(err) }()
if engine.isUnsafeTrustedProxies() {
debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +
"Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.")
}
address := resolveAddress(addr)
debugPrint("Listening and serving HTTP on %s\n", address)
err = http.ListenAndServe(address, engine.Handler())
return
}
简单几行代码,就能实现一个web服务。使用gin的Default方法创建一个路由handler。然后通过HTTP方法绑定路由规则和路由函数。不同于net/http库的路由函数,gin进行了封装,把request和response都封装到gin.Context的上下文环境。最后是启动路由的Run方法监听端口。麻雀虽小,五脏俱全。当然,除了GET方法,gin也支持POST,PUT,DELETE,OPTION等常用的restful方法。
Gin框架的路由
// 默认服务器
router.Run()
// 除了默认服务器中router.Run()的方式外,还可以用http.ListenAndServe(),比如
func main() {
router := gin.Default()
http.ListenAndServe(":8080", router)
}
// 自定义HTTP服务器的配置
func main() {
router := gin.Default()
s := &http.Server{
Addr: ":8080",
Handler: router,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20,
}
s.ListenAndServe()
}
路由
基本路由 gin 框架中采用的路由库是 httprouter。
// 创建带有默认中间件的路由:
// 日志与恢复中间件
router := gin.Default()
//创建不带中间件的路由:
//r := gin.New()
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的路由来自httprouter库。因此httprouter具有的功能,gin也具有,不过gin不支持路由正则表达式。
API参数
api 参数通过Context的Param方法来获取。
router.GET("/user/:name", func(c *gin.Context) {
name := c.Param("name")
c.String(http.StatusOK, "Hello, %s", name)
})
运行后浏览器输入:http://127.0.0.1:8000/user/jiejaitt后可以在游览器看到输出Hello, jiejaitt
。
冒号:
加上一个参数名组成路由参数。可以使用c.Params的方法读取其值。当然这个值是字串string。诸如/user/jiejaitt
,和/user/hello
都可以匹配,而/user/
和/user/jiejaitt/
不会被匹配。
router.GET("/user/:name/*action", func(c *gin.Context) {
name := c.Param("name")
action := c.Param("action")
message := name + " is " + action
c.String(http.StatusOK, message)
})
浏览器中输入:http://127.0.0.1:8000/user/jiejaitt/send。游览器输出jiejaitt is /send
。除了:
,gin还提供了*
号处理参数,*
号能匹配的规则就更多。
URL参数
web提供的服务通常是client和server的交互。其中客户端向服务器发送请求,除了路由参数,其他的参数无非两种,查询字符串query string和报文体body参数。所谓query string,即路由用,用?
以后连接的key1=value2&key2=value2
的形式的参数。当然这个key-value是经过urlencode编码。
URL 参数通过 DefaultQuery 或 Query 方法获取。
对于参数的处理,经常会出现参数不存在的情况,对于是否提供默认值,gin也考虑了,并且给出了一个优雅的方案,使用c.DefaultQuery方法读取参数,其中当参数不存在的时候,提供一个默认值。使用Query方法读取正常参数,当参数不存在的时候,返回空字串。
router.GET("/welcome", func(c *gin.Context) {
//可设置默认值
name := c.DefaultQuery("name","Guest")
//nickname := c.Query("nickname")
// 是 c.Request.URL.Query().Get("nickname") 的简写
c.String(http.StatusOK, "Hello, %s", name)
})
当浏览器输入的url为:http://127.0.0.1:8000/welcome?name=jiejaitt输出Hello, jiejaitt
。我们可以看到能够显示我们的参数数据,如果没有传递参数,那么就会显示默认值,url为:http://127.0.0.1:9527/welcome,输出为Hello, Guest
。
表单参数
http的报文体传输数据就比query string稍微复杂一点,常见的格式就有四种。例如application/json
,application/x-www-form-urlencoded
,application/xml
和multipart/form-data
。后面一个主要用于图片上传。json格式的很好理解,urlencode其实也不难,无非就是把query string的内容,放到了body体里,同样也需要urlencode。默认情况下,c.PostFROM解析的是x-www-form-urlencoded
或from-data
的参数。
表单参数通过 PostForm 方法获取:
router.POST("/form", func(c *gin.Context) {
//可设置默认值
type1 := c.DefaultPostForm("type", "alert")
username := c.PostForm("username")
password := c.PostForm("password")
//hobbys := c.PostFormMap("hobby")
//hobbys := c.QueryArray("hobby")
hobbys := c.PostFormArray("hobby")
c.String(http.StatusOK, fmt.Sprintf("type is %s, username is %s, password is %s,hobby is %v", type1, username, password, hobbys))
})
我们还需要提供一个html页面(login.html),来进行post请求:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
<form action="http://127.0.0.1:8000/form" method="post" enctype="application/x-www-form-urlencoded">
用户名:<input type="text" name="username">
<br>
密   码:<input type="password" name="password">
<br>
兴   趣:
<input type="checkbox" value="girl" name="hobby">女人
<input type="checkbox" value="game" name="hobby">游戏
<input type="checkbox" value="money" name="hobby">金钱
<br>
<input type="submit" value="登录">
</form>
</body>
</html>
然后运行程序后,通过浏览器访问页面,输入用户名和密码后,点击按钮进行登录。username和password数据我们可以获取,type获取不到就使用默认值。
使用PostForm形式,注意必须要设置Post的type,同时此方法中忽略URL中带的参数,所有的参数需要从Body中获得。
文件上传
上传单个文件
前面介绍了基本的发送数据,其中multipart/form-data
转用于文件上传。gin文件上传也很方便,和原生的net/http方法类似,不同在于gin把原生的request封装到c.Request中了。
首先我们创建一个go文件,demo06_file.go:
func main() {
router := gin.Default()
// Set a lower memory limit for multipart forms (default is 32 MiB)
// router.MaxMultipartMemory = 8 << 20 // 8 MiB
router.POST("/upload", func(c *gin.Context) {
// single file
file, _ := c.FormFile("file")
log.Println(file.Filename)
// Upload the file to specific dst.
c.SaveUploadedFile(file, file.Filename)
/*
也可以直接使用io操作,拷贝文件数据。
out, err := os.Create(filename)
defer out.Close()
_, err = io.Copy(out, file)
*/
c.String(http.StatusOK, fmt.Sprintf("'%s' uploaded!", file.Filename))
})
router.Run(":8080")
}
使用c.Request.FormFile
解析客户端文件name属性。如果不传文件,则会抛错,因此需要处理这个错误。此处我们略写了错误处理。一种是直接用c.SaveUploadedFile()保存文件。另一种方式是使用os的操作,把文件数据复制到硬盘上。
然后我们创建一个html页面,file.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>文件</title>
</head>
<body>
<form action="http://127.0.0.1:8080/upload" method="post" enctype="multipart/form-data">
头像:
<input type="file" name="file">
<br>
<input type="submit" value="提交">
</form>
</body>
</html>
运行程序后,打开浏览器传递文件:
点击按钮后进行提交上传:
显示已经上传,我们可以在项目目录下查看文件:
我们可以看到已经上传成功了一张图片。
我们也可以使用终端命令访问http,上传文件,我们打算传这个视频:
打开终端,并输入以下命令:
curl -X POST http://127.0.0.1:8080/upload -F "file=@/Users/ruby/Documents/pro/momo.mp4" -H "Content-Type: multipart/form-data"
我们可以看到这个视频文件已经上传到了项目的目录下。
上传多个文件
所谓多个文件,无非就是多一次遍历文件,然后一次copy数据存储即可。
package main
import (
"github.com/gin-gonic/gin"
"net/http"
"fmt"
)
func main() {
router := gin.Default()
// Set a lower memory limit for multipart forms (default is 32 MiB)
router.MaxMultipartMemory = 8 << 20 // 8 MiB
//router.Static("/", "./public")
router.POST("/upload", func(c *gin.Context) {
// Multipart form
form, err := c.MultipartForm()
if err != nil {
c.String(http.StatusBadRequest, fmt.Sprintf("get form err: %s", err.Error()))
return
}
files := form.File["files"]
for _, file := range files {
if err := c.SaveUploadedFile(file, file.Filename); err != nil {
c.String(http.StatusBadRequest, fmt.Sprintf("upload file err: %s", err.Error()))
return
}
}
c.String(http.StatusOK, fmt.Sprintf("Uploaded successfully %d files ", len(files)))
})
router.Run(":8080")
}
然后我们提供一个html页面,当然也可以使用终端命令:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>文件s</title>
</head>
<body>
<h1>上传多个文件</h1>
<form action="http://127.0.0.1:8080/upload" method="post" enctype="multipart/form-data">
Files: <input type="file" name="files" multiple><br><br>
<input type="submit" value="提交">
</form>
</body>
</html>
然后启动程序后,打开浏览器,点击选择文件开始上传。最后打开一下项目目录,查看刚刚上传的文件:
使用终端命令也可以:
curl -X POST http://localhost:8080/upload \
-F "upload[]=@/Users/ruby/Documents/pro/aa.jpeg" \
-F "upload[]=@/Users/ruby/Documents/pro/ad.txt" \
-H "Content-Type: multipart/form-data"
与单个文件上传类似,只不过使用了c.Request.MultipartForm
得到文件句柄,再获取文件数据,然后遍历读写。
路由组
router group是为了方便一部分相同的URL的管理,新建一个go文件(demo08_group.go),
package main
import (
"github.com/gin-gonic/gin"
"net/http"
"fmt"
)
func main() {
router := gin.Default()
// Simple group: v1
v1 := router.Group("/v1")
{
v1.GET("/login", loginEndpoint)
v1.GET("/submit", submitEndpoint)
v1.POST("/read", readEndpoint)
}
// Simple group: v2
v2 := router.Group("/v2")
{
v2.POST("/login", loginEndpoint)
v2.POST("/submit", submitEndpoint)
v2.POST("/read", readEndpoint)
}
router.Run(":8080")
}
func loginEndpoint(c *gin.Context) {
name := c.DefaultQuery("name", "Guest") //可设置默认值
c.String(http.StatusOK, fmt.Sprintf("Hello %s \n", name))
}
func submitEndpoint(c *gin.Context) {
name := c.DefaultQuery("name", "Guest") //可设置默认值
c.String(http.StatusOK, fmt.Sprintf("Hello %s \n", name))
}
func readEndpoint(c *gin.Context) {
name := c.DefaultQuery("name", "Guest") //可设置默认值
c.String(http.StatusOK, fmt.Sprintf("Hello %s \n", name))
}
运行程序后,可以通过一个html页面访问,也可以通过终端使用命令直接访问,此处我们使用终端:
curl http://127.0.0.1:8080/v1/login?name=JIeJaitt
运行结果为:Hello JIeJaitt
。
附录完整源码:
package main
import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.GET("/", func(c *gin.Context) {
c.String(http.StatusOK, "Hello, World!")
})
router.GET("/user/:name", func(c *gin.Context) {
name := c.Param("name")
c.String(http.StatusOK, "Hello, %s", name)
})
router.GET("/user/:name/*action", func(c *gin.Context) {
name := c.Param("name")
action := c.Param("action")
message := name + " is " + action
c.String(http.StatusOK, message)
})
router.GET("/welcome", func(c *gin.Context) {
name := c.DefaultQuery("name", "Guest") //可设置默认值
//nickname := c.Query("nickname") // 是 c.Request.URL.Query().Get("nickname") 的简写
c.String(http.StatusOK, "Hello, %s", name)
})
router.POST("/form", func(c *gin.Context) {
//可设置默认值
type1 := c.DefaultPostForm("type", "alert")
username := c.PostForm("username")
password := c.PostForm("password")
//hobbys := c.PostFormMap("hobby")
//hobbys := c.QueryArray("hobby")
hobbys := c.PostFormArray("hobby")
c.String(http.StatusOK, fmt.Sprintf("type is %s, username is %s, password is %s,hobby is %v", type1, username, password, hobbys))
})
router.Run(":8000")
}
Gin框架_Model
数据解析绑定
模型绑定可以将请求体绑定给一个类型。目前Gin支持JSON、XML、YAML和标准表单值的绑定。简单来说,,就是根据Body数据类型,将数据赋值到指定的结构体变量中 (类似于序列化和反序列化) 。
Gin提供了两套绑定方法:
- Must bind
方法:Bind,
BindJSON,
BindXML,
BindQuery,
BindYAML
行为:这些方法使用MustBindWith。如果存在绑定错误,则用c终止请求,使用c.AbortWithError (400) .SetType (ErrorTypeBind)
即可。将响应状态代码设置为400,Content-Type header设置为text/plain;charset = utf - 8
。请注意,如果在此之后设置响应代码,将会受到警告:[GIN-debug][WARNING] Headers were already written. Wanted to override status code 400 with 422
将导致已经编写了警告[GIN-debug][warning]标头。如果想更好地控制行为,可以考虑使用ShouldBind等效方法。
- Should bind
方法:ShouldBind,
ShouldBindJSON,
ShouldBindXML,
ShouldBindQuery,
ShouldBindYAML
行为:这些方法使用ShouldBindWith。如果存在绑定错误,则返回错误,开发人员有责任适当地处理请求和错误。
注意,使用绑定方法时,Gin 会根据请求头中 Content-Type 来自动判断需要解析的类型。如果你明确绑定的类型,你可以不用自动推断,而用 BindWith 方法。 你也可以指定某字段是必需的。如果一个字段被binding:"required"
修饰而值却是空的,请求会失败并返回错误。
JSON绑定
JSON的绑定,其实就是将request中的Body中的数据按照JSON格式进行解析,解析后存储到结构体对象中。新建一个go文件,demo09_bind.go:
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
type Login struct {
User string `form:"username" json:"user" uri:"user" xml:"user" binding:"required"`
Password string `form:"password" json:"password" uri:"password" xml:"password" binding:"required"`
}
func main() {
router := gin.Default()
//1.binding JSON
// Example for binding JSON ({"user": "jiejaitt", "password": "123456"})
router.POST("/loginJSON", func(c *gin.Context) {
var json Login
//其实就是将request中的Body中的数据按照JSON格式解析到json变量中
if err := c.BindJSON(&json); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if json.User != "jiejaitt" || json.Password != "123456" {
c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
return
}
c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
})
// curl -v -X POST http://127.0.0.1:8000/loginJSON -H 'content-type:application/json' -d '{"user":"jiejaitt","password":"123456"}'
router.Run(":8000")
}
前面我们使用c.String返回响应,顾名思义则返回string类型。content-type是plain或者text。调用c.JSON则返回json数据。其中gin.H封装了生成json的方式,是一个强大的工具。使用golang可以像动态语言一样写字面量的json,对于嵌套json的实现,嵌套gin.H即可。
然后打开终端输入以下命令:
curl -v -X POST http://127.0.0.1:8000/loginJSON -H 'content-type:application/json' -d '{"user":"jiejaitt","password":"123456"}'
可以返回正确的结果。
jiejaitt@huangyingjiedeMacBook-Air learngo % curl -v -X POST http://127.0.0.1:8000/loginJSON -H 'content-type:application/json' -d '{"user":"jiejaitt","password":"123456"}'
Note: Unnecessary use of -X or --request, POST is already inferred.
* Trying 127.0.0.1:8000...
* Connected to 127.0.0.1 (127.0.0.1) port 8000 (#0)
> POST /loginJSON HTTP/1.1
> Host: 127.0.0.1:8000
> User-Agent: curl/7.87.0
> Accept: */*
> content-type:application/json
> Content-Length: 39
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Content-Type: application/json; charset=utf-8
< Date: Tue, 11 Apr 2023 15:38:34 GMT
< Content-Length: 30
<
* Connection #0 to host 127.0.0.1 left intact
{"status":"you are logged in"}%
假如我们传递的json中只有user数据:
curl -v -X POST http://127.0.0.1:8000/loginJSON -H 'content-type:application/json' -d '{"user":"jiejaitt"}'
那么会得到一个错误信息:
jiejaitt@huangyingjiedeMacBook-Air learngo % curl -v -X POST http://127.0.0.1:8000/loginJSON -H 'content-type:application/json' -d '{"user":"jiejaitt"}'
Note: Unnecessary use of -X or --request, POST is already inferred.
* Trying 127.0.0.1:8000...
* Connected to 127.0.0.1 (127.0.0.1) port 8000 (#0)
> POST /loginJSON HTTP/1.1
> Host: 127.0.0.1:8000
> User-Agent: curl/7.87.0
> Accept: */*
> content-type:application/json
> Content-Length: 19
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 400 Bad Request
< Date: Tue, 11 Apr 2023 15:41:00 GMT
< Content-Length: 100
< Content-Type: text/plain; charset=utf-8
<
* Connection #0 to host 127.0.0.1 left intact
{"error":"Key: 'Login.Password' Error:Field validation for 'Password' failed on the 'required' tag"}%
Form表单
其实本质是将c中的request中的body数据解析到form中。首先我们先看一下绑定普通表单的例子:
在之前的代码demo09上继续添加就行:
// 3. Form 绑定普通表单的例子
// Example for binding a HTML form (user=hanru&password=hanru123)
router.POST("/loginForm", func(c *gin.Context) {
var form Login
//方法一:对于FORM数据直接使用Bind函数, 默认使用使用form格式解析,if c.Bind(&form) == nil
// 根据请求头中 content-type 自动推断.
if err := c.Bind(&form); err != nil {
c.JSON(http.StatusBadRequest,gin.H{"error":err.Error()})
return
}
if form.User != "jiejaitt" || form.Password != "123456" {
c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
return
}
c.JSON(http.StatusOK,gin.H{"status":"you are logged in"})
})
html页面,我们可以使用之前的login.html,但是要记得修改action后的路径:http://127.0.0.1:8000/loginForm
router.POST("/login", func(c *gin.Context) {
var form Login
//方法二: 使用BindWith函数,如果你明确知道数据的类型
// 你可以显式声明来绑定多媒体表单:
// c.BindWith(&form, binding.Form)
// 或者使用自动推断:
if c.BindWith(&form, binding.Form) == nil {
if from.User == "user" && form.Password == "password" {
c.JSON(200, gin.H{"status": "you are logged in ..... "})
} else {
c.JSON(401, gin.H{"status": "unauthorized"})
}
})
Uri绑定
// 5.URI
router.GET("/:user/:password", func(c *gin.Context) {
var login Login
if err := c.ShouldBindUri(&login); err != nil {
c.JSON(400, gin.H{"msg": err})
return
}
c.JSON(200, gin.H{"username": login.User, "password": login.Password})
})
打开终端输入以下内容:
curl -v http://127.0.0.1:8000/jiejaitt/123456
运行结果:
jiejaitt@huangyingjiedeMacBook-Air learngo % curl -v http://127.0.0.1:8000/jiejaitt/123456
* Trying 127.0.0.1:8000...
* Connected to 127.0.0.1 (127.0.0.1) port 8000 (#0)
> GET /jiejaitt/123456 HTTP/1.1
> Host: 127.0.0.1:8000
> User-Agent: curl/7.87.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Content-Type: application/json; charset=utf-8
< Date: Tue, 11 Apr 2023 16:10:41 GMT
< Content-Length: 43
<
* Connection #0 to host 127.0.0.1 left intact
{"password":"123456","username":"jiejaitt"}%
附录完整源码
package main
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
)
type Login struct {
User string `form:"username" json:"user" uri:"user" xml:"user" binding:"required"`
Password string `form:"password" json:"password" uri:"password" xml:"password" binding:"required"`
}
func main() {
router := gin.Default()
//1.binding JSON
// Example for binding JSON ({"user": "jiejaitt", "password": "123456"})
router.POST("/loginJSON", func(c *gin.Context) {
var json Login
//其实就是将request中的Body中的数据按照JSON格式解析到json变量中
if err := c.BindJSON(&json); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if json.User != "jiejaitt" || json.Password != "123456" {
c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
return
}
c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
})
// 3. Form 绑定普通表单的例子
// Example for binding a HTML form (user=hanru&password=hanru123)
router.POST("/loginForm", func(c *gin.Context) {
var form Login
//方法一:对于FORM数据直接使用Bind函数, 默认使用使用form格式解析,if c.Bind(&form) == nil
// 根据请求头中 content-type 自动推断.
if err := c.Bind(&form); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if form.User != "jiejaitt" || form.Password != "123456" {
c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
return
}
c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
})
router.POST("/login", func(c *gin.Context) {
var form Login
//方法二: 使用BindWith函数,如果你明确知道数据的类型
// 你可以显式声明来绑定多媒体表单:
// c.BindWith(&form, binding.Form)
// 或者使用自动推断:
if c.BindWith(&form, binding.Form) == nil {
if form.User == "user" && form.Password == "password" {
c.JSON(200, gin.H{"status": "you are logged in ..... "})
} else {
c.JSON(401, gin.H{"status": "unauthorized"})
}
}
})
// curl -v http://127.0.0.1:8000/jiejaitt/123456
router.GET("/:user/:password", func(c *gin.Context) {
var login Login
if err := c.ShouldBindUri(&login); err != nil {
c.JSON(400, gin.H{"msg": err})
return
}
c.JSON(200, gin.H{"username": login.User, "password": login.Password})
})
// curl -v -X POST http://127.0.0.1:8000/loginJSON -H 'content-type:application/json' -d '{"user":"jiejaitt","password":"123456"}'
router.Run(":8000")
}
响应请求
既然请求可以使用不同的content-type,响应也如此。通常响应会有html,text,plain,json和xml等。 gin提供了很优雅的渲染方法。
JSON/XML/YAML渲染
package main
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/testdata/protoexample"
)
func main() {
router := gin.Default()
// gin.H is a shortcut for map[string]interface{}
router.GET("someJSON", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "hey", "status": http.StatusOK})
})
router.GET("moreJSON", func(c *gin.Context) {
// You also can use a struct
var message struct {
Name string `json:"user"`
Message string
Number int
}
message.Name = "jiejaitt"
message.Message = "hey"
message.Number = 123
// 注意 message.Name 变成了 "user" 字段
// 以下方式都会输出 : {"user": "jiejaitt", "Message": "hey", "Number": 123}
c.JSON(http.StatusOK, message)
})
router.GET("someXML", func(c *gin.Context) {
c.XML(http.StatusOK, gin.H{"user": "jiejaitt", "message": "hey", "status": http.StatusOK})
})
router.GET("someYAML", func(c *gin.Context) {
c.YAML(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK})
})
router.GET("someProtoBuf", func(c *gin.Context) {
reps := []int64{int64(1), int64(2)}
label := "test"
// The specific definition of protobuf is written in the testdata/protoexample file.
data := &protoexample.Test{
Label: &label,
Reps: reps,
}
// Note that data becomes binary data in the response
// Will output protoexample.Test protobuf serialized data
c.ProtoBuf(http.StatusOK, data)
})
// Listen and serve on 0.0.0.0:8080
router.Run(":8080")
}
http://127.0.0.1:8080/moreJSON
http://127.0.0.1:8080/someXML
http://127.0.0.1:8080/someProtoBuf
HTML模板渲染
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
//加载模板
/*
router.LoadHTMLGlob("templates/*")
//router.LoadHTMLFiles("templates/template1.html", "templates/template2.html")
//定义路由
router.GET("/index", func(c *gin.Context) {
//根据完整文件名渲染模板,并传递参数
c.HTML(http.StatusOK, "index.tmpl", gin.H{
"title": "Main website",
})
})
*/
router.LoadHTMLGlob("templates/**/*")
router.GET("/posts/index", func(c *gin.Context) {
c.HTML(http.StatusOK, "posts/index.tmpl", gin.H{
"title": "Posts",
})
c.HTML(http.StatusOK, "users/index.tmpl", gin.H{
"title": "Users",
})
})
router.Run(":8080")
}
gin支持加载HTML模板, 然后根据模板参数进行配置并返回相应的数据。
先要使用 LoadHTMLGlob() 或者 LoadHTMLFiles()方法来加载模板文件,新建一个go文件(demo11_html.go):
func main() {
router := gin.Default()
//加载模板
router.LoadHTMLGlob("templates/*")
//router.LoadHTMLFiles("templates/template1.html", "templates/template2.html")
//定义路由
router.GET("/index", func(c *gin.Context) {
//根据完整文件名渲染模板,并传递参数
c.HTML(http.StatusOK, "index.tmpl", gin.H{
"title": "Main website",
})
})
router.Run(":8080")
}
创建一个目录:templates,然后在该目录下创建一个模板文件:
templates/index.tmpl
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Html</title>
</head>
<body>
<h1>
{{ .title }}
</h1>
</body>
</html>
http://127.0.0.1:8080/index
不同文件夹下模板名字可以相同,此时需要 LoadHTMLGlob() 加载两层模板路径。
router.LoadHTMLGlob("templates/**/*")
router.GET("/posts/index", func(c *gin.Context) {
c.HTML(http.StatusOK, "posts/index.tmpl", gin.H{
"title": "Posts",
})
c.HTML(http.StatusOK, "users/index.tmpl", gin.H{
"title": "Users",
})
})
重启项目后,打开浏览器输入以下网址:http://127.0.0.1:8080/posts/index
gin也可以使用自定义的模板引擎,如下
import "html/template"
func main() {
router := gin.Default()
html := template.Must(template.ParseFiles("file1", "file2"))
router.SetHTMLTemplate(html)
router.Run(":8080")
}
文件响应
静态文件服务
可以向客户端展示本地的一些文件信息,例如显示某路径下地文件。服务端代码是:
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
router := gin.Default()
// 下面测试静态文件服务
// 显示当前文件夹下的所有文件/或者指定文件
router.StaticFS("/showDir", http.Dir("."))
router.StaticFS("/files", http.Dir("/bin"))
//Static提供给定文件系统根目录中的文件。
//router.Static("/files", "/bin")
router.StaticFile("/image", "./assets/miao.jpg")
router.Run(":8080")
}
打开浏览器,输入地址:http://127.0.0.1:8080/showDir,访问当前项目目录的内容
重新输入地址:http://127.0.0.1:8080/files,访问操作系统/bin的下的内容。
浏览器中重新输入地址:http://127.0.0.1:8080/image
重定向
新建一个go文件(demo13_redirect.go):
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
r := gin.Default()
r.GET("/redirect", func(c *gin.Context) {
//支持内部和外部的重定向
c.Redirect(http.StatusMovedPermanently, "http://www.baidu.com/")
})
r.Run(":8080")
}
打开浏览器输入:http://127.0.0.1:8080/redirect,我们可以看到通过访问的路径,可以重定向到百度地址。
同步异步
goroutine 机制可以方便地实现异步处理。当在中间件或处理程序中启动新的Goroutines时,你不应该在原始上下文使用它,你必须使用只读的副本。
新建一个go文件:
package main
import (
"time"
"github.com/gin-gonic/gin"
"log"
)
func main() {
r := gin.Default()
//1. 异步
r.GET("/long_async", func(c *gin.Context) {
// goroutine 中只能使用只读的上下文 c.Copy()
cCp := c.Copy()
go func() {
time.Sleep(5 * time.Second)
// 注意使用只读上下文
log.Println("Done! in path " + cCp.Request.URL.Path)
}()
})
//2. 同步
r.GET("/long_sync", func(c *gin.Context) {
time.Sleep(5 * time.Second)
// 注意可以使用原始上下文
log.Println("Done! in path " + c.Request.URL.Path)
})
// Listen and serve on 0.0.0.0:8080
r.Run(":8080")
}
启动程序,打开浏览器并输入网址:http://127.0.0.1:8080/long_sync,然后在控制台观察打印的结果:然后我们在浏览器中更改地址:http://127.0.0.1:8080/long_async,然后观察控制台中打印的内容:
Gin框架_中间件
golang的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
skippedNodes *[]skippedNode
// This mutex protects Keys map.
mu sync.RWMutex
// Keys is a key/value pair exclusively for the context of each request.
Keys map[string]any
// 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 caches the query result from c.Request.URL.Query().
queryCache url.Values
// formCache caches c.Request.PostForm, which 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
我们通过源码可以知道就是[]HandlerFunc
. 而它的签名正是:
type HandlerFunc func(*Context)
所以中间件和我们普通的HandlerFunc
没有任何区别对吧, 我们怎么写HandlerFunc
就可以怎么写一个中间件.
全局中间件
先定义一个中间件函数:
func MiddleWare() gin.HandlerFunc {
return func(c *gin.Context) {
t := time.Now()
fmt.Println("before middleware")
//设置request变量到Context的Key中,通过Get等函数可以取得
c.Set("request", "client_request")
//发送request之前
c.Next()
//发送requst之后
// 这个c.Write是ResponseWriter,我们可以获得状态等信息
status := c.Writer.Status()
fmt.Println("after middleware,", status)
t2 := time.Since(t)
fmt.Println("time:", t2)
}
}
该函数很简单,只会给c上下文添加一个属性,并赋值。后面的路由处理器,可以根据被中间件装饰后提取其值。需要注意,虽然名为全局中间件,只要注册中间件的过程之前设置的路由,将不会受注册的中间件所影响。只有注册了中间件一下代码的路由函数规则,才会被中间件装饰。
router := gin.Default()
router.Use(MiddleWare())
{
router.GET("/middleware", func(c *gin.Context) {
//获取gin上下文中的变量
request := c.MustGet("request").(string)
req, _ := c.Get("request")
fmt.Println("request:",request)
c.JSON(http.StatusOK, gin.H{
"middile_request": request,
"request": req,
})
})
}
router.Run(":8080")
使用router装饰中间件,然后在/middlerware即可读取request的值,注意在router.Use(MiddleWare())代码以上的路由函数,将不会有被中间件装饰的效果。
使用花括号包含被装饰的路由函数只是一个代码规范,即使没有被包含在内的路由函数,只要使用router进行路由,都等于被装饰了。想要区分权限范围,可以使用组返回的对象注册中间件。
运行项目,可以在终端输入命令进行访问,
curl http://127.0.0.1:8080/middleware
或者是在浏览器中输入网址进行访问:http://127.0.0.1:8080/middleware
然后在服务器端的运行结果如下:
# curl http://127.0.0.1:8080/middleware
before middleware...
resquest: client_request
after middleware, 200
time: 65.917µs
[GIN] 2023/04/12 - 22:38:59 | 200 | 70.333µs | 127.0.0.1 | GET "/middleware"
# curl http://127.0.0.1:8080/before
before middleware...
before middleware...
after middleware, 200
time: 23.458µs
after middleware, 200
time: 78.334µs
[GIN] 2023/04/12 - 22:45:25 | 200 | 80µs | 127.0.0.1 | GET "/before"
Next()方法
我们怎么解决一个请求和一个响应经过我们的中间件呢?神奇的语句出现了, 没错就是c.Next()
,所有中间件都有Request
和Response
的分水岭, 就是这个c.Next()
,否则没有办法传递中间件。
服务端使用Use方法导入middleware,当请求/middleware来到的时候,会执行MiddleWare(), 并且我们知道在GET注册的时候,同时注册了匿名函数,所有请看Logger函数中存在一个c.Next()的用法,它是取出所有的注册的函数都执行一遍,然后再回到本函数中,所以,本例中相当于是先执行了 c.Next()即注册的匿名函数,然后回到本函数继续执行, 所以本例的Print的输出顺序是:
fmt.Println(“before middleware”)
fmt.Println(“request:”,request)
fmt.Println(“after middleware,”, status)
fmt.Println(“time:”, t2)
如果将c.Next()放在fmt.Println(“after middleware,”, status)后面,那么fmt.Println(“after middleware,”, status)和fmt.Println(“request:”,request)执行的顺序就调换了。所以一切都取决于c.Next()执行的位置。c.Next()的核心代码如下:
// Next should be used only inside middleware.
// It executes the pending handlers in the chain inside the calling handler.
// See example in GitHub.
func (c *Context) Next() {
c.index++
for s := int8(len(c.handlers)); c.index < s; c.index++ {
c.handlers[c.index](c)
}
}
它其实是执行了后面所有的handlers。
一个请求过来,Gin
会主动调用c.Next()
一次。因为handlers
是slice
,所以后来者中间件会追加到尾部。这样就形成了形如m1(m2(f()))
的调用链。正如上面数字① ② 标注的一样, 我们会依次执行如下的调用:
m1① -> m2① -> f -> m2② -> m1②
另外,如果没有注册就使用MustGet
方法读取c的值将会抛错,可以使用Get方法取而代之。上面的注册装饰方式,会让所有下面所写的代码都默认使用了router的注册过的中间件。
单个路由中间件
当然,gin也提供了针对指定的路由函数进行注册。
router.GET("/before", MiddleWare(), func(c *gin.Context) {
request := c.MustGet("request").(string)
c.JSON(http.StatusOK, gin.H{
"middile_request": request,
})
})
把上述代码写在 router.Use(Middleware())之前,同样也能看见/before
被装饰了中间件。通过浏览器访问以下地址:
中间件实践
中间件最大的作用,莫过于用于一些记录log,错误handler,还有就是对部分接口的鉴权。下面就实现一个简易的鉴权中间件。
简单认证BasicAuth
关于使用gin.BasicAuth() middleware, 可以直接使用一个router group进行处理, 本质和上面的一样。
先定义私有数据:
// 模拟私有数据
var secrets = gin.H{
"hanru": gin.H{"email": "hanru@163.com", "phone": "123433"},
"wangergou": gin.H{"email": "wangergou@example.com", "phone": "666"},
"ruby": gin.H{"email": "ruby@guapa.com", "phone": "523443"},
}
然后使用 gin.BasicAuth 中间件,设置授权用户
authorized := r.Group("/admin", gin.BasicAuth(gin.Accounts{
"hanru": "hanru123",
"wangergou": "1234",
"ruby": "hello2",
"lucy": "4321",
}))
最后定义路由:
authorized.GET("/secrets", func(c *gin.Context) {
// 获取提交的用户名(AuthUserKey)
user := c.MustGet(gin.AuthUserKey).(string)
if secret, ok := secrets[user]; ok {
c.JSON(http.StatusOK, gin.H{"user": user, "secret": secret})
} else {
c.JSON(http.StatusOK, gin.H{"user": user, "secret": "NO SECRET :("})
}
})
然后启动项目,打开浏览器输入以下网址:http://127.0.0.1:8080/admin/secrets,然后会弹出一个登录框:
需要输入正确的用户名和密码:
# 终端自动输出
{"secret":{"email":"hanru@163.com","phone":"123433"},"user":"hanru"}
# 人为格式化
{
"secret": {
"email": "hanru@163.com",
"phone": "123433"
},
"user": "hanru"
}
总结
/ 1.全局中间件 router.Use(gin.Logger()) router.Use(gin.Recovery())
// 2.单路由的中间件,可以加任意多个 router.GET(“/benchmark”, MyMiddelware(), benchEndpoint)
// 3.群组路由的中间件 authorized := router.Group(“/”, MyMiddelware()) // 或者这样用: authorized := router.Group(“/”) authorized.Use(MyMiddelware()) { authorized.POST(“/login”, loginEndpoint) }
基于database/sql的CURD操作
对于Gin本身,并没有对数据库的操作,本文实现的是,通过http访问程序,然后操作mysql数据库库。
查询
我们以之前讲解mysql时所使用的数据表为例:
接下来,我们就查询这张表,并将查询的结果以json的形式,返回给客户端。
示例代码:
//定义User类型结构
type User struct {
Id int `json:"id"`
Username string `json:"username"`
Password string `json:"password"`
}
//定义一个getALL函数用于回去全部的信息
func getAll() (users []User, err error) {
//1.操作数据库
db, _ := sql.Open("mysql", "root:hanru1314@tcp(127.0.0.1:3306)/mytest?charset=utf8")
//错误检查
if err != nil {
log.Fatal(err.Error())
}
//推迟数据库连接的关闭
defer db.Close()
//2.查询
rows, err := db.Query("SELECT id, username, password FROM user_info")
if err != nil {
log.Fatal(err.Error())
}
for rows.Next() {
var user User
//遍历表中所有行的信息
rows.Scan(&user.Id, &user.Username, &user.Password)
//将user添加到users中
users = append(users, user)
}
//最后关闭连接
defer rows.Close()
return
}
首先定义对应数据表的结构体,然后就是操作数据库进行查询
接下来,我们写web访问:
//创建一个路由Handler
router := gin.Default()
//get方法的查询
router.GET("/user", func(c *gin.Context) {
users, err := getAll()
if err != nil {
log.Fatal(err)
}
//H is a shortcut for map[string]interface{}
c.JSON(http.StatusOK, gin.H{
"result": users,
"count": len(users),
})
})
router.Run(":8080")
启动项目后,通过浏览器访问:http://127.0.0.1:8080/user
插入数据
我们可以设计一个方法用于向数据库中添加数据:
//插入数据
func add(user User) (Id int, err error) {
//1.操作数据库
db, _ := sql.Open("mysql", "root:hanru1314@tcp(127.0.0.1:3306)/mytest?charset=utf8")
//错误检查
if err != nil {
log.Fatal(err.Error())
}
//推迟数据库连接的关闭
defer db.Close()
stmt, err := db.Prepare("INSERT INTO user_info(username, password) VALUES (?, ?)")
if err != nil {
return
}
//执行插入操作
rs, err := stmt.Exec(user.Username, user.Password)
if err != nil {
return
}
//返回插入的id
id, err := rs.LastInsertId()
if err != nil {
log.Fatalln(err)
}
//将id类型转换
Id = int(id)
defer stmt.Close()
return
}
然后我们添加一个POST的路由,当通过post请求的时候,我们向数据库中插入数据:
//利用post方法新增数据
router.POST("/add", func(c *gin.Context) {
var u User
err := c.Bind(&u)
if err != nil {
log.Fatal(err)
}
Id, err := add(u)
fmt.Print("id=", Id)
c.JSON(http.StatusOK, gin.H{
"message": fmt.Sprintf("%s 插入成功", u.Username),
})
})
因为我们需要绑定struct类型,所以还需要修改之前的User:
//定义User类型结构
type User struct {
Id int `json:"id" form:"id"`
Username string `json:"username" form:"username"`
Password string `json:"password" form:"password"`
}
然后使用postman,模拟Post请求,当然,你也可以使用终端通过curl命令,或者直接写个HTML页面来访问:
也可以查询数据库,查看数据是否插入成功:
修改数据
添加一个update方法用于修改数据,我们实现的是根据id修改其他的字段:
//修改数据
func update(user User) (rowsAffected int64, err error) {
//1.操作数据库
db, _ := sql.Open("mysql", "root:hanru1314@tcp(127.0.0.1:3306)/mytest?charset=utf8")
//错误检查
if err != nil {
log.Fatal(err.Error())
}
//推迟数据库连接的关闭
defer db.Close()
stmt, err := db.Prepare("UPDATE user_info SET username=?, password=? WHERE id=?")
if err != nil {
return
}
//执行修改操作
rs, err := stmt.Exec(user.Username, user.Password,user.Id)
if err != nil {
return
}
//返回插入的id
rowsAffected,err =rs.RowsAffected()
if err != nil {
log.Fatalln(err)
}
defer stmt.Close()
return
}
然后在main中新添加一个路由:
//利用put方法修改数据
router.PUT("/update", func(c *gin.Context) {
var u User
err := c.Bind(&u)
if err != nil {
log.Fatal(err)
}
num, err := update(u)
fmt.Print("num=", num)
c.JSON(http.StatusOK, gin.H{
"message": fmt.Sprintf("修改id: %d 成功", u.Id),
})
})
删除数据
我们可以根据Id删除一条数据,删除刚刚修改的id为10的数据,先添加一个delete方法:
//通过id删除
func del(id int) (rows int, err error) {
//1.操作数据库
db, _ := sql.Open("mysql", "root:hanru1314@tcp(127.0.0.1:3306)/mytest?charset=utf8")
//错误检查
if err != nil {
log.Fatal(err.Error())
}
//推迟数据库连接的关闭
defer db.Close()
stmt, err := db.Prepare("DELETE FROM user_info WHERE id=?")
if err != nil {
log.Fatalln(err)
}
rs, err := stmt.Exec(id)
if err != nil {
log.Fatalln(err)
}
//删除的行数
row, err := rs.RowsAffected()
if err != nil {
log.Fatalln(err)
}
defer stmt.Close()
rows = int(row)
return
}
然后再main中注册一个路由:
//利用DELETE请求方法通过id删除
router.DELETE("/delete/:id", func(c *gin.Context) {
id := c.Param("id")
Id, err := strconv.Atoi(id)
if err != nil {
log.Fatalln(err)
}
rows, err := del(Id)
if err != nil {
log.Fatalln(err)
}
fmt.Println("delete rows ", rows)
c.JSON(http.StatusOK, gin.H{
"message": fmt.Sprintf("Successfully deleted user: %s", id),
})
})
然后重新运行项目,使用postman模拟delete访问:
查看数据库,是否真正的删除,我们发现id为10的数据已经没有了: