Gin 基础使用 | 青训营

99 阅读4分钟

基础

1、定义

Gin 是一个基于 Go 语言的轻量级 Web 框架,用于构建高性能的 Web 应用程序

下载:go get -u github.com/gin-gonic/gin

1.1 示例

 package main
 ​
 import (
     "github.com/gin-gonic/gin"
 )
 ​
 func main() {
     // 创建一个默认的路由引擎
     r := gin.Default()
     // GET:请求方式;/hello:请求的路径
     // 当客户端以GET方法请求/hello路径时,会执行后面的匿名函数
     r.GET("/hello", func(c *gin.Context) {
         // c.JSON:返回JSON格式的数据
         c.JSON(200, gin.H{
             "message": "Hello world!",
         })
     })
     // 启动HTTP服务,默认在0.0.0.0:8080启动服务
     r.Run()
 }

2、渲染

2.1 静态文件处理

 package main
 ​
 import (
     "net/http"
 ​
     "github.com/gin-gonic/gin"
 )
 ​
 func main() {
     r := gin.Default()
     r.Static("/static", "./static")
     r.LoadHTMLGlob("templates/**/*")
 ​
     r.GET("/app", func(ctx *gin.Context) {
         ctx.HTML(http.StatusOK, "index.html", nil)
     })
 ​
     r.Run(":9090")
 }

静态文件服务会将 ./static 目录下的文件映射到 /statis 路径下。 这意味着当访问 /statis 路径时,Gin 框架会尝试在 ./static 目录下查找相应的静态文件并返回给客户端

image.png

2.2 JSON渲染

 func main() {
     r := gin.Default()
 ​
     // gin.H 是map[string]interface{}的缩写
     r.GET("/someJSON", func(c *gin.Context) {
         // 方式一:自己拼接JSON
         c.JSON(http.StatusOK, gin.H{"message": "Hello world!"})
     })
     r.GET("/moreJSON", func(c *gin.Context) {
         // 方法二:使用结构体
         var msg struct {
             Name    string `json:"user"`
             Message string
             Age     int
         }
         msg.Name = "小王子"
         msg.Message = "Hello world!"
         msg.Age = 18
         c.JSON(http.StatusOK, msg)
     })
     r.Run(":8080")
 }

3、获取参数

3.1 获取querystring参数

例:/user/search?username=小王子&address=沙河

 func main() {
     //Default返回一个默认的路由引擎
     r := gin.Default()
     r.GET("/user/search", func(c *gin.Context) {
         username := c.DefaultQuery("username", "小王子")
         //username := c.Query("username")
         address := c.Query("address")
         //输出json结果给调用方
         c.JSON(http.StatusOK, gin.H{
             "message":  "ok",
             "username": username,
             "address":  address,
         })
     })
     r.Run()
 }

3.2 获取param参数

例:/user/search/小王子/沙河

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

3.3 获取form参数

 func main() {
     //Default返回一个默认的路由引擎
     r := gin.Default()
     r.POST("/user/search", func(c *gin.Context) {
         // DefaultPostForm取不到值时会返回指定的默认值
         //username := c.DefaultPostForm("username", "小王子")
         username := c.PostForm("username")
         address := c.PostForm("address")
         //输出json结果给调用方
         c.JSON(http.StatusOK, gin.H{
             "message":  "ok",
             "username": username,
             "address":  address,
         })
     })
     r.Run(":8080")
 }

3.4 参数绑定

为了能够更方便的获取请求相关参数,提高开发效率,我们可以基于请求的Content-Type识别请求数据类型并利用反射机制自动提取请求中QueryStringform表单JSONXML等参数到结构体中

 // Binding from JSON
 type Login struct {
     User     string `form:"user" json:"user" binding:"required"`
     Password string `form:"password" json:"password" binding:"required"`
 }
 ​
 func main() {
     router := gin.Default()
 ​
     // 绑定JSON的示例 ({"user": "q1mi", "password": "123456"})
     router.POST("/loginJSON", func(c *gin.Context) {
         var login Login
         if err := c.ShouldBind(&login); err == nil {
             fmt.Printf("login info:%#v\n", login)
             c.JSON(http.StatusOK, gin.H{
                 "user":     login.User,
                 "password": login.Password,
             })
         } else {
             c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
         }
     })
 ​
     // 绑定form表单示例 (user=q1mi&password=123456)
     router.POST("/loginForm", func(c *gin.Context) {
         var login Login
         // ShouldBind()会根据请求的Content-Type自行选择绑定器
         if err := c.ShouldBind(&login); err == nil {
             c.JSON(http.StatusOK, gin.H{
                 "user":     login.User,
                 "password": login.Password,
             })
         } else {
             c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
         }
     })
 ​
     // 绑定QueryString示例 (/loginQuery?user=q1mi&password=123456)
     router.GET("/loginForm", func(c *gin.Context) {
         var login Login
         // ShouldBind()会根据请求的Content-Type自行选择绑定器
         if err := c.ShouldBind(&login); err == nil {
             c.JSON(http.StatusOK, gin.H{
                 "user":     login.User,
                 "password": login.Password,
             })
         } else {
             c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
         }
     })
 ​
     // Listen and serve on 0.0.0.0:8080
     router.Run(":8080")
 }

ShouldBind会按照下面的顺序解析请求中的数据完成绑定:传入参数是 &login

  1. 如果是 GET 请求,只使用 Form 绑定引擎(query)。
  2. 如果是 POST 请求,首先检查 content-type 是否为 JSONXML,然后再使用 Formform-data)。

4、文件上传

4.1 简单文件上传

前端页面

 <!DOCTYPE html>
 <html lang="zh-CN">
 <head>
     <title>上传文件示例</title>
 </head>
 <body>
 <form action="/upload" method="post" enctype="multipart/form-data">
     <input type="file" name="f1">
     <input type="submit" value="上传">
 </form>
 </body>
 </html>

后端代码

func main() {
	router := gin.Default()
	// 处理multipart forms提交文件时默认的内存限制是32 MiB
	// 可以通过下面的方式修改
	// router.MaxMultipartMemory = 8 << 20  // 8 MiB
	router.POST("/upload", func(c *gin.Context) {
		// 单个文件
		file, err := c.FormFile("f1")
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{
				"message": err.Error(),
			})
			return
		}

		log.Println(file.Filename)
		dst := fmt.Sprintf("C:/tmp/%s", file.Filename)
		// 上传文件到指定的目录
		c.SaveUploadedFile(file, dst)
		c.JSON(http.StatusOK, gin.H{
			"message": fmt.Sprintf("'%s' uploaded!", file.Filename),
		})
	})
	router.Run()
}

4.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()
}

5、重定向

5.1 HTTP重定向

HTTP 重定向很容易。 内部、外部重定向均支持。

 r.GET("/test", func(c *gin.Context) {
     c.Redirect(http.StatusMovedPermanently, "http://www.sogo.com/")
 })

5.2 路由重定向

路由重定向,使用HandleContext

 r.GET("/test", func(c *gin.Context) {
     // 指定重定向的URL
     c.Request.URL.Path = "/test2"
     r.HandleContext(c)
 })
 r.GET("/test2", func(c *gin.Context) {
     c.JSON(http.StatusOK, gin.H{"hello": "world"})
 })

6、路由

6.1 普通路由

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

image.png 可以匹配所有请求方法的Any方法

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

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

6.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版本时。

7、中间件

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

Gin中的中间件必须是一个gin.HandlerFunc类型。

7.1 入门

 // StatCost 是一个统计耗时请求耗时的中间件
 func StatCost() gin.HandlerFunc {
     return func(c *gin.Context) {
         start := time.Now()
         c.Set("name", "小王子") // 可以通过c.Set在请求上下文中设置值,后续的处理函数能够取到该值
         // 调用该请求的剩余处理程序
         c.Next()
         // 不调用该请求的剩余处理程序
         // c.Abort()
         // 计算耗时
         cost := time.Since(start)
         log.Println(cost)
     }
 }
 ​
 ​
 func main(){
     r := gin.Default()
     
     r.GET("/test", StatCoast(),func(c *gin.Context) {
         name := c.MustGet("name").(string) // 从上下文取值
         log.Println(name)
         c.JSON(http.StatusOK, gin.H{
             "message": "Hello world!",
         })
     })
     r.Run()
 }

7.2 执行原理

类似一条责任链形式,c.Next 包含下一个中间件函数

image.png

6.3 中间件使用

通过闭包【函数+外层引用】的形式,做准备工作

image.png

7.4 注册中间件

  1. 为全局路由注册中间件:r.Use(StatCost())

  2. 为单个路由注册中间件

    1.   // 给 /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!",
                })
            })
      
  3. 为路由组注册中间件

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

7.5 默认中间件

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())。