Go语言基础笔记(五)| 青训营笔记

152 阅读8分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第5篇笔记。

6、gin框架

使用:

  1. 下载gin依赖go get github.com/gin-gonic/gin
  2. 导入ginimport "github.com/gin-gonic/gin"

6.1 gin渲染

  1. HTML渲染
  2. 自定义模板函数
  3. 静态文件处理

main.go文件

 package main
 ​
 import (
     "github.com/gin-gonic/gin"
     "html/template"
     "net/http"
 )
 ​
 func sayHello(c *gin.Context){
     c.JSON(200,gin.H{
         "message": "hello,golang!",
     })
 ​
 }
 ​
 func main() {
 ​
     r := gin.Default()//返回默认的路由引擎
 ​
     //静态文件加载
     r.Static("/xxx","./statics")
 ​
     //gin框架中给模板添加自定义函数
     r.SetFuncMap(template.FuncMap{
         "safe": func(str string) template.HTML{
             return template.HTML(str)
         },
     })
 ​
     // r.LoadHTMLFiles("templates/posts/index.tmpl")//模板解析
     r.LoadHTMLGlob("templates/**/*")
 ​
     r.GET("/posts/index",func (c *gin.Context)  {
         c.HTML(http.StatusOK,"posts/index.tmpl",gin.H{
             "title": "www.posts.com",
         })
     })
 ​
     r.GET("/users/index",func (c *gin.Context)  {
         c.HTML(http.StatusOK,"users/index.tmpl",gin.H{
             "title": "<a href='https://www.qq.com'>qq首页</a>",
         })
     })
 ​
     //加载网上下载的前端模板
     r.GET("/car", func(c *gin.Context) {
         c.HTML(http.StatusOK,"home.html",nil)
     })
     //启动服务
     r.Run(":8080")
 }

6.1.1 JSON渲染

 package main
 ​
 import (
     "github.com/gin-gonic/gin"
     "net/http"
 )
 ​
 func main() {
     //Default返回一个默认的路由引擎
     r := gin.Default()
 ​
     //gin.H 是map[string]interface{}的缩写
     r.GET("/test1", func(c *gin.Context) {
         //data := map[string]interface{}{
         //  "username":"yg",
         //  "age":12,
         //}
         //c.JSON(http.StatusOK,data)
 ​
         //gin.H  等效于 map[string]interface{}
 ​
         c.JSON(http.StatusOK,gin.H{
             "username": "yg",
             "age": 12,
         })
 ​
     })
 ​
     r.GET("/test2", func(c *gin.Context) {
         // 方法二:使用结构体
         //属性首字母必须大写 为了序列化
         var msg struct {
             Name    string `json:"user"`  //输出json格式的时候为此名
             Message string
             Age     int
         }
         msg.Name = "yg"
         msg.Message = "Hello golang!"
         msg.Age = 18
         c.JSON(http.StatusOK, msg) //json序列化
     })
 ​
     //启动服务
     r.Run()//默认8080端口   修改端口的话传入参数例如":9999"
 }
 ​

6.2 获取参数

6.2.1 获取querystring参数

GET请求 URL?后面的是querystring参数

querystring指的是URL中?后面携带的参数,例如:/user/get?username=yg&age=12。 获取请求的querystring参数的方法如下:

 package main
 ​
 import (
     "github.com/gin-gonic/gin"
     "net/http"
 )
 ​
 func main() {
     //Default返回一个默认的路由引擎
     r := gin.Default()
 ​
     //querystring
     r.GET("/user/get", func(c *gin.Context) {
         username := c.DefaultQuery("username","yg")
         age := c.Query("age")
         sex, ok := c.GetQuery("sex")//取不到第二个参数 返回false
 ​
         c.JSON(http.StatusOK,gin.H{
             "message": "success",
             "username": username,
             "age": age,
             "sex": sex,
             "sex_isok": ok,
         })
     })
 ​
     //启动服务
     r.Run()
 }

6.2.2 获取form表单参数

 func main() {
     //Default返回一个默认的路由引擎
     r := gin.Default()
 ​
     //form表单数据
     r.POST("/user/get", func(c *gin.Context) {
         // DefaultPostForm取不到值时会返回指定的默认值
         //username := c.DefaultPostForm("username", "yg")
         username := c.PostForm("username")
         age := c.PostForm("age")
         //输出json结果给调用方
         c.JSON(http.StatusOK, gin.H{
             "message":  "ok",
             "username": username,
             "age":  age,
         })
     })
 ​
     //启动服务
     r.Run()
 }

6.2.3 获取json参数

 func main() {
     //Default返回一个默认的路由引擎
     r := gin.Default()
 ​
     //json参数
     r.POST("/json", func(c *gin.Context) {
         // 注意:下面为了举例子方便,暂时忽略了错误处理
         b, _ := c.GetRawData()  // 从c.Request.Body读取请求数据
         // 定义map或结构体
         var m map[string]interface{}
         // 反序列化
         _ = json.Unmarshal(b, &m)
 ​
         c.JSON(http.StatusOK, m)
     })
 ​
     //启动服务
     r.Run()
 }

6.2.4 获取path参数

请求的参数通过URL路径传递,例如:/user/search/yg/18。 获取请求URL路径中的参数的方式如下。

 func main() {
     //Default返回一个默认的路由引擎
     r := gin.Default()
 ​
     //path参数
     r.GET("/user/get/:username/:age", func(c *gin.Context) {
         username := c.Param("username")
         age := c.Param("age")
         //输出json结果给调用方
         c.JSON(http.StatusOK, gin.H{
             "message":  "ok",
             "username": username,
             "address":  age,
         })
     })
     //启动服务
     r.Run()
 }

6.2.5 参数绑定

为了能够更方便的获取请求相关参数,提高开发效率,我们可以基于请求的Content-Type识别请求数据类型并利用反射机制自动提取请求中QueryStringform表单JSONXML等参数到结构体中。 下面的示例代码演示了.ShouldBind()强大的功能,它能够基于请求自动提取JSONform表单QueryString类型的数据,并把值绑定到指定的结构体对象。

ShouldBind会按照下面的顺序解析请求中的数据完成绑定:

  1. 如果是 GET 请求,只使用 Form 绑定引擎(query)。
  2. 如果是 POST 请求,首先检查 content-type 是否为 JSONXML,然后再使用 Formform-data)。
 package main
 ​
 import (
     "fmt"
     "github.com/gin-gonic/gin"
     "net/http"
 )
 type UserInfo struct {
     Username string `form:"username"  json:"username"`
     Password string `form:"password"  json:"password"`
 }
 ​
 func main() {
     //Default返回一个默认的路由引擎
     r := gin.Default()
 ​
     //form表单
     r.POST("test1", func(c *gin.Context) {
 ​
         var u UserInfo //
         //使用ShouldBind必须传入指针进去才能完成赋值
         //底层通过反射
         err := c.ShouldBind(&u)
         if err != nil {
             c.JSON(http.StatusBadRequest,gin.H{
                 "error": err.Error(),
             })
         }else{
             fmt.Printf("%#v\n",u)
             c.JSON(http.StatusOK,gin.H{
                 "status":"ok",
             })
         }
 ​
     })
 ​
     //绑定json
     r.POST("test2", func(c *gin.Context) {
         var u UserInfo
 ​
         if err := c.ShouldBind(&u);err!=nil{
             c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
         }else{
             fmt.Printf("UserInfo: %#v\n",u)
             c.JSON(http.StatusOK,gin.H{
                 "status": "success",
                 "username": u.Username,
                 "password": u.Password,
             })
         }
     })
     
     //启动服务
     r.Run()
 }

6.3 上传文件

6.3.1 单文件上传

 package main
 ​
 import (
     "fmt"
     "github.com/gin-gonic/gin"
     "log"
     "net/http"
     "path"
 )
 ​
 func main() {
     r := gin.Default()
     r.LoadHTMLFiles("./index.html")
     r.GET("/index", func(c *gin.Context) {
         c.HTML(http.StatusOK,"index.html",nil)
     })
 ​
     // 处理multipart forms提交文件时默认的内存限制是32 MiB
     // 可以通过下面的方式修改
     // r.MaxMultipartMemory = 8 << 20  // 8 MiB
     r.POST("/upload", func(c *gin.Context) {
         // 单个文件
         //从请求中读取文件
         file, err := c.FormFile("f1")  //与请求参数中的name一致
         if err != nil {
             c.JSON(http.StatusInternalServerError, gin.H{
                 "message": err.Error(),
             })
             return
         }
 ​
         log.Println(file.Filename)
         //将读取的文件保存在本地
         //dst := fmt.Sprintf("./%s", file.Filename)
         dst := path.Join("./",file.Filename)
         // 上传文件到指定的目录
         c.SaveUploadedFile(file, dst)
         c.JSON(http.StatusOK, gin.H{
             "message": fmt.Sprintf("'%s' uploaded!", file.Filename),
         })
     })
     r.Run()
 }

6.3.2 多文件上传

 func main() {
     router := gin.Default()
     // 处理multipart forms提交文件时默认的内存限制是32 MiB
     // 可以通过下面的方式修改
     // router.MaxMultipartMemory = 8 << 20  // 8 MiB
     router.POST("/upload", func(c *gin.Context) {
         // Multipart form
         form, _ := c.MultipartForm()
         files := form.File["file"]
 ​
         for index, file := range files {
             log.Println(file.Filename)
             dst := fmt.Sprintf("C:/tmp/%s_%d", file.Filename, index)
             // 上传文件到指定的目录
             c.SaveUploadedFile(file, dst)
         }
         c.JSON(http.StatusOK, gin.H{
             "message": fmt.Sprintf("%d files uploaded!", len(files)),
         })
     })
     router.Run()
 }

6.4 重定向

6.4.1 HTTP重定向

 func main() {
     r := gin.Default()
 ​
     r.GET("/index", func(c *gin.Context) {
 ​
         //c.JSON(http.StatusOK,gin.H{
         //  "status":"ok",
         //})
         c.Redirect(http.StatusMovedPermanently,"http://www.qq.com")
     })
     
     r.Run()
 }

6.4.2 路由重定向

路由重定向,使用HandleContext

 func main() {
     r := gin.Default()
 ​
 ​
     r.GET("/a", func(c *gin.Context) {
         // 指定重定向的URL
         //跳转到 /b 对应的路由处理函数
         c.Request.URL.Path = "/b"  //把请求的URI修改
         r.HandleContext(c) //继续后续的处理
     })
 ​
     r.GET("/b", func(c *gin.Context) {
         c.JSON(http.StatusOK,gin.H{
             "message": "已重定向至此处",
         })
     })
 ​
     r.Run()
 }

6.5 Gin路由

6.5.1 普通路由

 r.GET("/index", func(c *gin.Context) {...})
 r.GET("/login", func(c *gin.Context) {...})
 r.POST("/login", func(c *gin.Context) {...})

此外,还有一个可以匹配所有请求方法的Any方法如下:

 r.Any("/test", func(c *gin.Context) {
     switch c.Request.Method {
         case "GET":
         c.JSON(http.StatusOK,gin.H{"methodName": "GET"})
         case "POST":
         c.JSON(http.StatusOK,gin.H{"methodName": "POST"})
         case "PUT":
         c.JSON(http.StatusOK,gin.H{"methodName": "PUT"})
         case "DELETE":
         c.JSON(http.StatusOK,gin.H{"methodName": "DELETE"})
         default:
         c.JSON(http.StatusNotFound,gin.H{"ERROR":"error method"})
     }
 })

为没有配置处理函数的路由添加处理程序,默认情况下它返回404代码,下面的代码为没有匹配到路由的请求都返回views/404.html页面。

 r.NoRoute(func(c *gin.Context) {
         c.HTML(http.StatusNotFound, "views/404.html", nil)
     })

6.5.2 路由组

我们可以将拥有共同URL前缀的路由划分为一个路由组。习惯性一对{}包裹同组的路由,这只是为了看着清晰,你用不用{}包裹功能上没什么区别。

 func main() {
     r := gin.Default()
     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) {...})
     }
     r.Run()
 }

路由组也是支持嵌套的,例如:

 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) {...})
         // 嵌套路由组
         xx := shopGroup.Group("xx")
         xx.GET("/oo", func(c *gin.Context) {...})
     }

通常我们将路由分组用在划分业务逻辑或划分API版本时。

6.5.3 路由原理

Gin框架中的路由使用的是httprouter这个库。

其基本原理就是构造一个路由地址的前缀树。

6.6 Gin中间件

Gin框架允许开发者在处理请求的过程中,加入用户自己的钩子(Hook)函数。这个钩子函数就叫中间件,中间件适合处理一些公共的业务逻辑,比如登录认证、权限校验、数据分页、记录日志、耗时统计等。

6.6.1 定义中间件

Gin中的中间件必须是一个gin.HandlerFunc类型。例如我们像下面的代码一样定义一个统计请求耗时的中间件。

 // StatCost 是一个统计耗时请求耗时的中间件
 func StatCost() gin.HandlerFunc {
     return func(c *gin.Context) {
         start := time.Now()
         c.Set("name", "yg") // 可以通过c.Set在请求上下文中设置值,后续的处理函数能够取到该值
         // 调用该请求的剩余处理程序
         c.Next()
         // 不调用该请求的剩余处理程序
         // c.Abort()
         // 计算耗时
         cost := time.Since(start)
         log.Println(cost)
     }
 }

6.6.2 注册中间件

在gin框架中,我们可以为每个路由添加任意数量的中间件。

为全局路由注册

 func main() {
     // 新建一个没有任何默认中间件的路由
     r := gin.New()
     // 注册一个全局中间件
     r.Use(StatCost())
     
     r.GET("/test", func(c *gin.Context) {
         name := c.MustGet("name").(string) // 从上下文取值
         log.Println(name)
         c.JSON(http.StatusOK, gin.H{
             "message": "Hello world!",
         })
     })
     r.Run()
 }

为某个路由单独注册

 // 给/test2路由单独注册中间件(可注册多个)
     r.GET("/test2", StatCost(), func(c *gin.Context) {
         name := c.MustGet("name").(string) // 从上下文取值
         log.Println(name)
         c.JSON(http.StatusOK, gin.H{
             "message": "Hello world!",
         })
     })

为路由组注册中间件

为路由组注册中间件有以下两种写法。

写法1:

 shopGroup := r.Group("/shop", StatCost())
 {
     shopGroup.GET("/index", func(c *gin.Context) {...})
     ...
 }

写法2:

 shopGroup := r.Group("/shop")
 shopGroup.Use(StatCost())
 {
     shopGroup.GET("/index", func(c *gin.Context) {...})
     ...
 }

6.6.3 中间件注意事项

gin默认中间件

gin.Default()默认使用了LoggerRecovery中间件,其中:

  • Logger中间件将日志写入gin.DefaultWriter,即使配置了GIN_MODE=release
  • Recovery中间件会recover任何panic。如果有panic的话,会写入500响应码。

如果不想使用上面两个默认的中间件,可以使用gin.New()新建一个没有任何默认中间件的路由。

gin中间件中使用goroutine

当在中间件或handler中启动新的goroutine时,不能使用原始的上下文(c *gin.Context),必须使用其只读副本(c.Copy())。

 package main
 ​
 import (
     "fmt"
     "github.com/gin-gonic/gin"
     "net/http"
     "time"
 )
 ​
 func testHandler1(c *gin.Context) {
     fmt.Println("index1....")
     if name,e := c.Get("name");e==true{
         fmt.Println(name)
     }
     c.JSON(http.StatusOK,gin.H{
         "status": 200,
         "message": "index",
     })
 }
 //定义一个中间件m1: 统计请求处理函数的耗时
 func m1(c *gin.Context){
     fmt.Println("m1 in....")
     //计时
     start := time.Now()
     c.Next() //调用后续的处理函数
     //c.Abort() //阻止调用后续的处理函数
     cost := time.Since(start)
     fmt.Printf("cost:%v\n",cost)
     fmt.Println("m1 out....")
 }
 ​
 func m2(c *gin.Context){
     fmt.Println("m2  in....")
     c.Set("name","yg")
     c.Next() //调用后续的处理函数
     //c.Abort() //阻止调用后续的处理函数
     //return //结束
     fmt.Println("m2 out....")
 }
 ​
 func authMiddle(doCheck bool) gin.HandlerFunc{
     //连接数据库
     //其他准备工作
     return func(c *gin.Context){
         //是否登录的判断
         if doCheck{
 ​
         }else{
 ​
         }
         c.Next()
         //if 是登录用户
         //c.Next()
         //else
         //c.Abort()
     }
 }
 ​
 func main() {
     r := gin.Default()
 ​
     r.Use(m1,m2,authMiddle(true))  //全局注册中间件函数m1
 ​
     r.GET("/index1", testHandler1)
     r.GET("/shop", func(c *gin.Context) {
         c.JSON(http.StatusOK,gin.H{
             "message": "shop",
         })
     })
     r.GET("/user", func(c *gin.Context) {
         c.JSON(http.StatusOK,gin.H{
             "message": "user",
         })
     })
 ​
     //404
     r.NoRoute(func(c *gin.Context) {
         c.JSON(http.StatusNotFound,gin.H{
             "message": "网页不存在,请联系管理员~~~",
         })
     })
 ​
     r.Run()
 }

6.6.4 运行多个服务

我们可以在多个端口启动服务,例如:

 package main
 ​
 import (
     "log"
     "net/http"
     "time"
 ​
     "github.com/gin-gonic/gin"
     "golang.org/x/sync/errgroup"
 )
 ​
 var (
     g errgroup.Group
 )
 ​
 func router01() http.Handler {
     e := gin.New()
     e.Use(gin.Recovery())
     e.GET("/", func(c *gin.Context) {
         c.JSON(
             http.StatusOK,
             gin.H{
                 "code":  http.StatusOK,
                 "error": "Welcome server 01",
             },
         )
     })
 ​
     return e
 }
 ​
 func router02() http.Handler {
     e := gin.New()
     e.Use(gin.Recovery())
     e.GET("/", func(c *gin.Context) {
         c.JSON(
             http.StatusOK,
             gin.H{
                 "code":  http.StatusOK,
                 "error": "Welcome server 02",
             },
         )
     })
 ​
     return e
 }
 ​
 func main() {
     server01 := &http.Server{
         Addr:         ":8080",
         Handler:      router01(),
         ReadTimeout:  5 * time.Second,
         WriteTimeout: 10 * time.Second,
     }
 ​
     server02 := &http.Server{
         Addr:         ":8081",
         Handler:      router02(),
         ReadTimeout:  5 * time.Second,
         WriteTimeout: 10 * time.Second,
     }
    // 借助errgroup.Group或者自行开启两个goroutine分别启动两个服务
     g.Go(func() error {
         return server01.ListenAndServe()
     })
 ​
     g.Go(func() error {
         return server02.ListenAndServe()
     })
 ​
     if err := g.Wait(); err != nil {
         log.Fatal(err)
     }
 }