1. Gin简单入门
Gin 框架 现在是 github 上 start 最多 Go 语言编写的 Web 框架,相比其他它的几个 start 数量差不多的框架,它更加的轻量,有更好的性能。它使用了httprouter,如果你是性能和高效的追求者, 你会爱上Gin!
1.1. 简单示例
1.1.1. 第一个gin程序
import (
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
)
func main() {
r := gin.Default() // 创建默认路由引擎
// 配置 location 为 / 时,且请求为GET时的路由处理函数
r.GET("/", func(c *gin.Context) {
// c.JSON() 构造json格式的响应体,在后端应用中非常常见
// 请求和响应的响应信息都封装在了对象 c 中
c.JSON(200, gin.H{
"method" : c.Request.Method,
"location": c.Request.URL.String(),
"message": "Hello world!",
})
})
err := r.Run("0.0.0.0:8080") // 绑定 8080 端口
if err != nil {
log.Fatal("Init gin server failed,err:%s", err.Error())
}
}
[root@duduniao ~]# curl 127.0.0.1:8080
{"location":"/","message":"Hello world!","method":"GET"}
1.1.2. RestFul API
在后端开发中,RestFul API 非常常见,即location就是目标资源,而不同的请求方法代表对这个请求资源的操作方式,如:
请求方法 | URL | 含义 |
---|---|---|
GET | /book/ISBN6379 | 查询书籍信息 |
HEADA | /book/ISBN6379 | 查询书籍是否存在 |
POST | /book/ISBN6379 | 创建书籍记录 |
PUT | /book/ISBN6379 | 更新书籍信息 |
DELETE | /book/ISBN6379 | 删除书籍信息 |
import (
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
)
func main() {
r := gin.Default()
// GET
r.GET("/book/ISBN6379", func(c *gin.Context) {
c.JSON(200, gin.H{
"method": c.Request.Method,
})
})
// POST
r.POST("/book/ISBN6379", func(c *gin.Context) {
c.JSON(200, gin.H{
"method": c.Request.Method,
})
})
// PUT
r.PUT("/book/ISBN6379", func(c *gin.Context) {
c.JSON(200, gin.H{
"method": c.Request.Method,
})
})
// DELETE
r.DELETE("/book/ISBN6379", func(c *gin.Context) {
c.JSON(200, gin.H{
"method": c.Request.Method,
})
})
if err := r.Run("0.0.0.0:8080"); err != nil {
log.Fatal("Init web failed, err:%s\n", err.Error())
}
}
[root@duduniao ~]# for i in GET POST PUT DELETE;do curl -X $i http://127.0.0.1:8080/book/ISBN6379;echo ;done
{"method":"GET"}
{"method":"POST"}
{"method":"PUT"}
{"method":"DELETE"}
1.2. Gin渲染
所谓渲染即生成响应体,在后端开发中,常用的响应体有 json,html,protobuf,静态文件等。
1.2.1. Json 渲染
func main() {
r := gin.Default()
// 使用 gin.H{} 构造json
r.GET("/", func(c *gin.Context) {
c.JSON(200, gin.H{
"method": c.Request.Method,
"location": c.Request.URL.String(),
"message": "root page",
})
})
// 使用结构体
r.GET("/index", func(c *gin.Context) {
res := struct {
Method string `json:"method"`
Location string `json:"location"`
Message string `json:"message"`
}{c.Request.Method, c.Request.URL.Path, "index page"}
c.JSON(200, &res)
})
err := r.Run("0.0.0.0:8080")
if err != nil {
log.Fatal("Init gin server failed,err:%s", err.Error())
}
}
[root@duduniao ~]# curl http://127.0.0.1:8080
{"location":"/","message":"root page","method":"GET"}
[root@duduniao ~]# curl http://127.0.0.1:8080/index
{"method":"GET","location":"/index","message":"index page"}
1.2.2. YAML渲染
func main() {
r := gin.Default()
// 使用 gin.H{} 构造json
r.GET("/", func(c *gin.Context) {
c.YAML(200, gin.H{
"method": c.Request.Method,
"location": c.Request.URL.String(),
"message": "root page",
})
})
// 使用结构体
r.GET("/index", func(c *gin.Context) {
res := struct {
Method string
Location string
Message string
}{c.Request.Method, c.Request.URL.Path, "index page"}
c.YAML(200, &res)
})
err := r.Run("0.0.0.0:8080")
if err != nil {
log.Fatal("Init gin server failed,err:%s", err.Error())
}
}
[root@duduniao ~]# curl http://127.0.0.1:8080/index
method: GET
location: /index
message: index page
[root@duduniao ~]# curl http://127.0.0.1:8080
location: /
message: root page
method: GET
1.2.3. XML渲染
与json和yaml返回不同,XML不支持返回匿名结构体对象
func main() {
r := gin.Default()
// 使用 gin.H{} 构造json
r.GET("/", func(c *gin.Context) {
c.XML(200, gin.H{
"method": c.Request.Method,
"location": c.Request.URL.String(),
"message": "root page",
})
})
// 使用结构体
r.GET("/index", func(c *gin.Context) {
// 使用匿名结构体会无法显示
type msg struct {
Method string
Location string
Message string
}
var res = msg{c.Request.Method, c.Request.URL.Path, "index page"}
c.XML(200, &res)
})
err := r.Run("0.0.0.0:8080")
if err != nil {
log.Fatal("Init gin server failed,err:%s", err.Error())
}
}
[root@duduniao ~]# curl http://127.0.0.1:8080/index
<msg><Method>GET</Method><Location>/index</Location><Message>index page</Message></msg>
[root@duduniao ~]# curl http://127.0.0.1:8080
<map><method>GET</method><location>/</location><message>root page</message></map>
1.2.4. HTML渲染
在做web开发时,返回渲染的 html 页面常见很常见,一般流程: 在 templates 目录下创建html模板文件 —> 导入html模板文件 —> 执行 c.HTML()渲染
[root@duduniao gin_basic]# tree
.
├── html.go
└── templates
├── book
│ └── index.html
└── user
└── index.html
{{define "user/index.html"}}
<!DOCTYPE html>
<html lang="en">
<body>
<h1>{{.name}}</h1>
<h1>{{.age}}</h1>
</body>
</html>
{{end}}
{{define "book/index.html"}}
<!DOCTYPE html>
<html lang="en">
<body>
<h2>{{.title}}</h2>
</body>
</html>
{{end}}
package main
import (
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
)
func main() {
r := gin.Default()
r.LoadHTMLGlob("templates/**/*.html")
r.GET("/book/", func(c *gin.Context) {
c.HTML(200, "book/index.html", gin.H{"title": "books"})
})
r.GET("/user/", func(c *gin.Context) {
c.HTML(200, "user/index.html", gin.H{"name":"张三", "age":18})
})
err := r.Run("0.0.0.0:8080")
if err != nil {
log.Fatal("Init gin server failed,err:%s", err.Error())
}
}
1.3. 请求处理
1.3.1. PATH参数处理
所谓PATH参数解析,就是指解析URL的locaction,如从请求 GET /student/201909011208
获取学生的ID 201909011208。PATH中关键词解析是通过占位符来实现的。
func main() {
r := gin.Default()
r.GET("/user/search/:name/:age", func(c *gin.Context) {
name, age := c.Param("name"), c.Param("age")
c.JSON(200, gin.H{
"name": name,
"age": age,
})
})
if err := r.Run("0.0.0.0:80"); err != nil {
log.Fatal("Init web failed,err:%s", err.Error())
}
}
[root@duduniao ~]# curl http://127.0.0.1/user/search/zhangsan/20
{"age":"20","name":"zhangsan"}
[root@duduniao ~]# curl http://127.0.0.1/user/search/张三/20
{"age":"20","name":"张三"}
1.3.2. GET请求参数
在非 RESTAPI 请求中,请求基本都是通过GET加URL参数完成的,比如 GET /user/search?name=zhangsan&age=20
,因此对请求URL中的参数解析场景很常见,Gin中采用 c.Query(key)
和 c.DefaultQuery(key,default)
获取,如果对于的key不存在,前者返回空字符串,后者范围指定的default。
func main() {
r := gin.Default()
r.GET("/user/search", func(c *gin.Context) {
name := c.Query("name")
age := c.Query("age")
class := c.DefaultQuery("class", "101") // 如果不存在则使用默认值
c.JSON(200, gin.H{
"name": name,
"age": age,
"class": class,
})
})
if err := r.Run("0.0.0.0:80"); err != nil {
log.Fatal("Init web failed,err:%s", err.Error())
}
}
[root@duduniao ~]# curl "http://127.0.0.1/user/search?name=zhangsan&age=20"
{"age":"20","class":"101","name":"zhangsan"}
[root@duduniao ~]# curl "http://127.0.0.1/user/search?name=zhangsan"
{"age":"","class":"101","name":"zhangsan"}
1.3.3. 获取表单参数
在网站中开发中,用户提交注册或者登陆信息一般通过POST表单进行传递,Gin中获取该参数的值是通过 c.PostForm(key)
实现。
func main() {
r := gin.Default()
r.POST("/user/search", func(c *gin.Context) {
name := c.PostForm("name")
age := c.PostForm("age")
class := c.PostForm("class")
c.JSON(200, gin.H{
"name": name,
"age": age,
"class": class,
})
})
if err := r.Run("0.0.0.0:80"); err != nil {
log.Fatal("Init web failed,err:%s", err.Error())
}
}
[root@duduniao ~]# curl -X POST "http://127.0.0.1/user/search" -d "name=zhangsan&age=19"
{"age":"19","class":"","name":"zhangsan"}
[root@duduniao ~]# curl -X POST "http://127.0.0.1/user/search" -d "name=zhangsan&age=19&class=301"
{"age":"19","class":"301","name":"zhangsan"}
1.3.4. 获取json参数
获取GET请求参数和表单参数有快捷方式,如上面两个案例所示,但是json类的请求体需要使用 c.BindJson(&obj)
来取参数。
func main() {
r := gin.Default()
r.POST("/user/search", func(c *gin.Context) {
var user UserInfo
err := c.BindJSON(&user) // 绑定json解析,query,yaml,xml同理
if err != nil {
log.Errorf("Get args failed, err:%s\n", err.Error())
c.JSON(200, gin.H{"err": err.Error()})
return
}
c.JSON(200, gin.H{
"name": user.Name,
"age": user.Age,
"class": user.Class,
})
})
if err := r.Run("0.0.0.0:80"); err != nil {
log.Fatal("Init web failed,err:", err.Error())
}
}
[root@duduniao ~]# curl -X POST -H "Content-Type:application/json" -d '{"name":"zhangsan","age":20}' http://127.0.0.1/user/search
{"age":20,"class":"","name":"zhangsan"}
[root@duduniao ~]# curl -X POST -H "Content-Type:application/json" -d '{"name":"zhangsan","age":20,"class":"312"}' http://127.0.0.1/user/search
{"age":20,"class":"312","name":"zhangsan"}
1.3.5. 参数自动绑定
上面的案例中,都是明确知道是URL传参,或者POST表单,或者Json请求体。但是Gin中还提供了一种自动判断参数类型,并解析到结构体对象中的方法。可以自动根据请求类型、Content-Type 来判断请求数据的类型,解析后绑定到指定的结构体中。需要注意的是,结构体中必须打上 form 的tag,否则GET请求中的参数无法解析成功
type UserInfo struct {
Name string `form:"name" json:"name"`
Age uint8 `form:"age" json:"age"`
Class string `form:"class" json:"class"`
}
func search(c *gin.Context) {
var user UserInfo
err := c.ShouldBind(&user) // 根据 Content-Type 自动进行解析
if err != nil {
log.Errorf("Get args failed, err:%s\n", err.Error())
c.JSON(200, gin.H{"err": err.Error()})
return
}
c.JSON(200, gin.H{
"method": c.Request.Method,
"name": user.Name,
"age": user.Age,
"class": user.Class,
})
}
func main() {
r := gin.Default()
r.POST("/user/search", search)
r.GET("/user/search", search)
if err := r.Run("0.0.0.0:80"); err != nil {
log.Fatal("Init web failed,err:", err.Error())
}
}
[root@duduniao ~]# curl "http://127.0.0.1/user/search?name=zhangsan&age=20&class=301"
{"age":20,"class":"301","method":"GET","name":"zhangsan"}
[root@duduniao ~]# curl -X POST "http://127.0.0.1/user/search" -d "name=zhangsan&age=19&class=301"
{"age":19,"class":"301","method":"POST","name":"zhangsan"}
[root@duduniao ~]# curl -X POST -H "Content-Type:application/json" -d '{"name":"zhangsan","age":20,"class":"312"}' http://127.0.0.1/user/search
{"age":20,"class":"312","method":"POST","name":"zhangsan"}
1.4. 上传的文件处理
1.4.1. 上传单个文件
func main() {
r := gin.Default()
r.POST("/upload/", func(c *gin.Context) {
file, err := c.FormFile("background") // 上传单个文件,明确知晓文件的 key
if err != nil {
c.JSON(400, gin.H{"status": 411, "msg": "recv file failed,err" + err.Error()})
log.Errorf("recv file failed,err:%s\n", err.Error())
return
}
if err := c.SaveUploadedFile(file, file.Filename); err != nil {
c.JSON(400, gin.H{"status": 412, "msg": "save file failed, err" + err.Error()})
log.Errorf("save %s failed,err:%s\n", file.Filename, err.Error())
return
}
log.Infof("recv %s success\n", file.Filename)
c.JSON(200, gin.H{"status": 200, "msg": "revc success"})
})
if err := r.Run("0.0.0.0:80"); err != nil {
log.Fatal("Init web failed,err:", err.Error())
}
}
[root@duduniao 壁纸]# curl -X POST -F 'background=@2020-03-14_22-40-07.jpg' http://127.0.0.1/upload/
{"msg":"revc success","status":200}
1.4.2. 上传多个文件
func main() {
r := gin.Default()
// 设置能接收的缓冲区大小
r.MaxMultipartMemory = 50 * 1024 * 1024
r.POST("/upload/", func(c *gin.Context) {
form, err := c.MultipartForm() // 获取表单信息
if err != nil {
c.JSON(400, gin.H{"status":411,"msg": "recv file failed, err:" + err.Error()})
log.Errorf("get files failed,err:%s\n",err.Error())
return
}
files, ok := form.File["files"] // 获取表单中的文件列表
if !ok {
c.JSON(400, gin.H{"status":412,"msg":"recv file failed, err: no key names files"})
log.Errorf("get files failed,err:%s\n","no key names files")
return
}
// 变量文件对象并写入本地
for index, file := range files {
if err := c.SaveUploadedFile(file, fmt.Sprintf("%d-%s", index, file.Filename)); err != nil {
c.JSON(400, gin.H{"status":412,"msg":"save file failed, err:" + err.Error()})
log.Errorf("save %s failed,err:%s\n",file.Filename, err.Error())
return
}
log.Infof("save %s success\n",file.Filename)
}
log.Infof("recv %s success\n", "file.Filename")
c.JSON(200, gin.H{"status":200,"msg":"revc success"})
})
if err := r.Run("0.0.0.0:80"); err != nil {
log.Fatal("Init web failed,err:", err.Error())
}
}
[root@duduniao 壁纸]# curl -X POST -F 'files=@2020-03-10_10-06-06.jpg' -F 'files=@2020-03-10_10-09-32.jpg' -F 'files=@2020-03-14_22-41-02.jpg' http://127.0.0.1/upload/
{"msg":"revc success","status":200}
1.5. 重定向
在HTTP协议中,重定向分为两种: 301 永久重定向,302 临时重定向。在Gin中,重定向有两种实现的方式,一种是是通过HTTP重定向,指定返回码(301,302)和目标地址即可;另一种是通过修改路由实现,内部进行URL跳转,类似于nginx的rewrite规则。
1.5.1. HTTP重定向
func main() {
r := gin.Default()
r.GET("/book/", func(c *gin.Context) {
// c.Redirect(301, "/books/") // 永久重定向
c.Redirect(302, "/books/") // 临时重定向
})
r.GET("/books/", func(c *gin.Context) {
c.JSON(200, gin.H{
"method" : c.Request.Method,
"location": c.Request.URL.String(),
"message": "Hello world!",
})
})
err := r.Run("0.0.0.0:8080")
if err != nil {
log.Fatal("Init gin server failed,err:%s", err.Error())
}
}
[root@duduniao ~]# curl -Lv http://127.0.0.1:8080/book/
......
> GET /book/ HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.58.0
> Accept: */*
>
< HTTP/1.1 302 Found
< Content-Type: text/html; charset=utf-8
< Location: /books/
< Date: Sat, 13 Jun 2020 13:52:54 GMT
< Content-Length: 30
<
......
> GET /books/ HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.58.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: application/json; charset=utf-8
< Date: Sat, 13 Jun 2020 13:52:54 GMT
< Content-Length: 62
<
* Connection #0 to host 127.0.0.1 left intact
{"location":"/books/","message":"Hello world!","method":"GET"}
1.5.2. 路由重定向
func main() {
r := gin.Default()
r.GET("/book/", func(c *gin.Context) {
c.Request.URL.Path = "/books/"
r.HandleContext(c)
})
r.GET("/books/", func(c *gin.Context) {
c.JSON(200, gin.H{
"method" : c.Request.Method,
"location": c.Request.URL.String(),
"message": "Hello world!",
})
})
err := r.Run("0.0.0.0:8080")
if err != nil {
log.Fatal("Init gin server failed,err:%s", err.Error())
}
}
[root@duduniao ~]# curl -Lv http://127.0.0.1:8080/book/
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
> GET /book/ HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.58.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: application/json; charset=utf-8
< Date: Sat, 13 Jun 2020 13:57:18 GMT
< Content-Length: 62
<
* Connection #0 to host 127.0.0.1 left intact
{"location":"/books/","message":"Hello world!","method":"GET"}
1.6. Gin路由
路由就是请求(请求方法+URL)和处理函数之间的关系绑定,根据业务场景,可以分为普通路由和路由组,普通路由就是一个一个零散的路由,路由组是若干个有关联关系的路由组成,比如对同一个URL的不同方法,同一个API版本的所有路由集合等,路由组习惯性一对{}
包裹同组的路由,这只是为了看着清晰,你用不用{}
包裹功能上没什么区别。
1.6.1. 普通路由
r.GET("/index", func(c *gin.Context) {...})
r.PUT("/index", func(c *gin.Context) {...})
r.POST("/index", func(c *gin.Context) {...})
r.Any("/index", func(c *gin.Context) {...}) // 匹配任意方法
r.NoRoute(func(c *gin.Context) {...}) // 没有任何匹配项
1.6.2. 路由组
userGroup := r.Group("/user")
{
userGroup.GET("/index", func(c *gin.Context) {...})
userGroup.GET("/login", func(c *gin.Context) {...})
userGroup.POST("/login", func(c *gin.Context) {...})
}
shopGroup := r.Group("/shop")
{
shopGroup.GET("/index", func(c *gin.Context) {...})
shopGroup.GET("/cart", func(c *gin.Context) {...})
shopGroup.POST("/checkout", func(c *gin.Context) {...})
}
1.7. 中间件
Gin框架允许开发者在处理请求的过程中,加入用户自己的钩子(Hook)函数。这个钩子函数就叫中间件,中间件适合处理一些公共的业务逻辑,比如登录认证、权限校验、数据分页、记录日志、耗时统计等。
1.7.1. 定义中间件
Gin中的中间件必须是一个gin.HandlerFunc类型的函数,因此定义中间件就是定义返回值为 gin.HandlerFunc 的函数。中间件类似于一个装饰器,其中 c.Next() 表示执行被装饰的函数,即路由处理函数。
func timer() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Next() // 执行
log.Infof("The request [%s %s] const %v\n", c.Request.Method, c.Request.URL.Path, time.Since(start))
}
}
1.7.2. 注册中间件
中间件的注册分为三种类型:
- 注册全局中间件,即所有的请求都会调用该中间件进行装饰:
r.Use(timer())
- 为某个特定路由注册,支持多个中间件:
r.GET("/", timer(), func(c *gin.context) {} )
- 为路由组注册中间件,可以在定义路由组的时候注册:
userGroup := r.Group("/user", timer2())
也可以对定义好的路由组添加中间件: deployGroup.Use(timer2())
全局中间件和路由中间件,执行顺序,参考一下案例:
import (
"fmt"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
"time"
)
func response(c *gin.Context) {
time.Sleep(time.Millisecond * 100)
c.JSON(200, gin.H{"message": "ok", "location": c.Request.URL.String()})
}
func timer1() gin.HandlerFunc {
return func(c *gin.Context) {
fmt.Println("timer1 start")
start := time.Now()
c.Next()
log.Infof("[timer1] The request [%s %s] const %v\n", c.Request.Method, c.Request.URL.Path, time.Since(start))
fmt.Println("timer1 stop")
}
}
func timer2() gin.HandlerFunc {
return func(c *gin.Context) {
fmt.Println("timer2 start")
start := time.Now()
c.Next()
log.Infof("[timer2] The request [%s %s] const %v\n", c.Request.Method, c.Request.URL.Path, time.Since(start))
fmt.Println("timer2 stop")
}
}
func main() {
r := gin.Default()
r.Use(timer1()) // 注册全局中间件
r.GET("/index", timer2(), response) // 为特定路由注册中间件
userGroup := r.Group("/user", timer2()) // 为路由组添加中间件
{
userGroup.GET("/index", response)
userGroup.GET("/edit", response)
}
deployGroup := r.Group("/deploy")
deployGroup.Use(timer2()) // 为路由组添加中间件
{
deployGroup.GET("/deployment", response)
deployGroup.GET("/service", response)
deployGroup.GET("/template", response)
}
if err := r.Run("0.0.0.0:80"); err != nil {
log.Fatalf("Init gin failed, err:%s\n", err.Error())
}
}
[root@duduniao ~]# curl http://127.0.0.1/index
{"location":"/index","message":"ok"}
------
timer1 start
timer2 start
INFO[0009] [timer2] The request [GET /index] const 100.7118ms
timer2 stop
INFO[0009] [timer1] The request [GET /index] const 100.8172ms
timer1 stop
[root@duduniao ~]# curl http://127.0.0.1/user/edit
{"location":"/user/edit","message":"ok"}
------
timer1 start
timer2 start
INFO[0018] [timer2] The request [GET /user/edit] const 100.5929ms
timer2 stop
INFO[0018] [timer1] The request [GET /user/edit] const 100.6653ms
timer1 stop
[root@duduniao ~]# curl http://127.0.0.1/deploy/service
{"location":"/deploy/service","message":"ok"}
------
timer1 start
timer2 start
INFO[0026] [timer2] The request [GET /deploy/service] const 100.3718ms
timer2 stop
INFO[0026] [timer1] The request [GET /deploy/service] const 100.4596ms
timer1 stop
1.7.3. 中间件注意事项
- gin默认中间件
gin.Default()默认使用了Logger和Recovery中间件,其中:
- Logger中间件将日志写入gin.DefaultWriter,即使配置了GIN_MODE=release。
- Recovery中间件会recover任何panic。如果有panic的话,会写入500响应码。
如果不想使用上面两个默认的中间件,可以使用gin.New()新建一个没有任何默认中间件的路由。
- gin中间件中使用goroutine
当在中间件或handler中启动新的goroutine时,不能使用原始的上下文(c *gin.Context),必须使用其只读副本(c.Copy())