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()// GETr.GET("/book/ISBN6379", func(c *gin.Context) {c.JSON(200, gin.H{"method": c.Request.Method,})})// POSTr.POST("/book/ISBN6379", func(c *gin.Context) {c.JSON(200, gin.H{"method": c.Request.Method,})})// PUTr.PUT("/book/ISBN6379", func(c *gin.Context) {c.JSON(200, gin.H{"method": c.Request.Method,})})// DELETEr.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{} 构造jsonr.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{} 构造jsonr.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 stringLocation stringMessage 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/indexmethod: GETlocation: /indexmessage: index page[root@duduniao ~]# curl http://127.0.0.1:8080location: /message: root pagemethod: GET
1.2.3. XML渲染
与json和yaml返回不同,XML不支持返回匿名结构体对象
func main() {r := gin.Default()// 使用 gin.H{} 构造jsonr.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 stringLocation stringMessage 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 mainimport ("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 UserInfoerr := 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 UserInfoerr := 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") // 上传单个文件,明确知晓文件的 keyif 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 * 1024r.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 starttimer2 startINFO[0009] [timer2] The request [GET /index] const 100.7118mstimer2 stopINFO[0009] [timer1] The request [GET /index] const 100.8172mstimer1 stop[root@duduniao ~]# curl http://127.0.0.1/user/edit{"location":"/user/edit","message":"ok"}------timer1 starttimer2 startINFO[0018] [timer2] The request [GET /user/edit] const 100.5929mstimer2 stopINFO[0018] [timer1] The request [GET /user/edit] const 100.6653mstimer1 stop[root@duduniao ~]# curl http://127.0.0.1/deploy/service{"location":"/deploy/service","message":"ok"}------timer1 starttimer2 startINFO[0026] [timer2] The request [GET /deploy/service] const 100.3718mstimer2 stopINFO[0026] [timer1] The request [GET /deploy/service] const 100.4596mstimer1 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())
