likes
comments
collection
share

gin 基本使用

作者站长头像
站长
· 阅读数 47

gin 初体验

import (
  "net/http"
  "github.com/gin-gonic/gin"
)

func main() {
  r := gin.Default()
  r.GET("/ping", func(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{
      "message": "pong",
    })
  })
  r.Run()
}

gin 路由接受一个 type HandlerFunc func(Context) 类型的函数

New 和 Default 的区别

gin.Newgin.Default 都可以创建一个类型为 *gin.Enginerouter

他们的区别是,gin.Default 加了两个中间件:Logger(), Recovery()

路由分组

路由分组功能是将相同功能的路由进行分组,方便管理

r := gin.Default()
r.GET("/goods/list", goodList)
r.GET("/goods/1", goodDetail)
r.POST("goods/add", createGood)
func goodList(c *gin.Context)   {}
func goodDetail(c *gin.Context) {}
func createGood(c *gin.Context) {}
r := gin.Default()
goodsGroup := r.Group("/goods")
{
  goodsGroup.GET("/list", goodList)
  goodsGroup.GET("/1", goodDetail)
  goodsGroup.POST("/add", createGood)
}

url 中的变量

要获取 url 中的变量,使用 :xxx 的形式

func main() {
  r := gin.Default()
  goodsGroup := r.Group("/goods")
  {
    goodsGroup.GET("/:id", goodDetail)
  }
  r.Run()
}

func goodDetail(c *gin.Context) {
  id := c.Param("id")
  c.JSON(http.StatusOK, gin.H{
    "message": "id: " + id,
  })
}

这种形式会有一个问题,如果有两个路由,一个是 /goods/list,一个是 /goods/:id,那么 /goods/list 会被 /goods/:id 匹配到

解决办法是使用 goodsGroup.GET("/list", goodList),这样就不会有问题了

func main() {
  r := gin.Default()
  goodsGroup := r.Group("/goods")
  {
    goodsGroup.GET("/list", goodList)
    goodsGroup.GET("/:id", goodDetail)
  }
  r.Run()
}

func goodList(c *gin.Context) {
  c.JSON(http.StatusOK, gin.H{
    "message": "list",
  })
}

func goodDetail(c *gin.Context) {
  id := c.Param("id")
  c.JSON(http.StatusOK, gin.H{
    "message": "id: " + id,
  })
}

但是其他路由还是会进入到 /goods/:id 中,比如 /goods/detail

如果只想匹配 id 是数字,需要这样做

通过一个结构体来绑定 uri 中的参数,在注册函数中使用 ShouldBindUri 方法来绑定,如果不是绑定的类型,就返回错误

type Params struct {
	ID int `uri:"id" binding:"required"`
}

func main() {
	r := gin.Default()
	goodsGroup := r.Group("/goods")
	{
		goodsGroup.GET("/:id", goodDetail)
	}

	r.Run()
}
func goodDetail(c *gin.Context) {
	id := c.Param("id")
	var params Params
	if err := c.ShouldBindUri(&params); err != nil {
		c.Status(http.StatusBadRequest)
		return
	}
	c.JSON(http.StatusOK, gin.H{
		"message": "id: " + id,
	})
}

还有一种形式是使用 * 来匹配,比如 /goods/*name

如果访问的路由是 /goods/1/2/3/4,那么 id 就是 1name 就是 /2/3/4,一般用来访问服务器上的文件

goodsGroup.GET("/:id/*name", goodPersoon)
func goodPerson(c *gin.Context) {
  id := c.Param("id")
  name := c.Param("name")
  c.JSON(http.StatusOK, gin.H{
    "id": id,
    "name": name,
  })
}

获取请求中的参数

获取 query 参数,可以使用 c.Query("key"),如果没有这个参数,就返回空字符串

如果想要获取 query 参数,但是没有这个参数,就返回默认值,可以使用 c.DefaultQuery("key", "default")

page := c.DefaultQuery("page", "1")
size := c.Query("size")

获取 body 参数,可以使用 c.PostForm("key"),如果没有这个参数,就返回空字符串

如果想要获取 body 参数,但是没有这个参数,就返回默认值,可以使用 c.DefaultPostForm("key", "default")

name := c.DefaultPostForm("name", "default")
age := c.PostForm("age")

PostForm 是针对 Content-Typeapplication/x-www-form-urlencodedapplication/form-data 的情况

如果请求参数是 application/json,那么需要使用 c.ShouldBindJSON/c.BindJSON 方法来获取参数

type Body struct {
	Name string `json:"name"`
	Age  int    `json:"age"`
}

func goodAdd(c *gin.Context) {
	var body Body
	c.BindJSON(&body)
	c.JSON(http.StatusOK, gin.H{
		"name": body.Name,
		"age":  body.Age,
	})
}

表单验证

表单验证可以直接使用 binding 标签来实现

gin 内置了 validator,文档:validator

注册时,需要输入两次密码,可以使用 eqfield 来验证两次密码是否一致

type SignUpForm struct {
  Age        uint8  `json:"age" binding:"required,gte=1,lte=130"`
  Name       string `json:"name" binding:"required,min=3"`
  Email      string `json:"email" binding:"required,email"`
  Password   string `json:"password" binding:"required"`
  RePassword string `json:"re_password" binding:"required,eqfield=Password"`
}
r.POST("/signup", func(c *gin.Context) {
  var signUpFrom SignUpForm
  if err := c.ShouldBindJSON(&signUpFrom); err != nil {
    c.JSON(http.StatusBadRequest, gin.H{
      "error": err.Error(),
    })
    return
  }
  c.JSON(http.StatusOK, gin.H{
    "message": "ok",
  })
})

错误信息翻译

定义 InitTrans 函数,用来初始化翻译器

import (
  "github.com/gin-gonic/gin"
  "github.com/gin-gonic/gin/binding"
  "github.com/go-playground/locales/en"
  "github.com/go-playground/locales/zh"
  ut "github.com/go-playground/universal-translator"
  "github.com/go-playground/validator/v10"
  en_translations "github.com/go-playground/validator/v10/translations/en"
  zh_translations "github.com/go-playground/validator/v10/translations/zh"
)
var trans ut.Translator
func InitTrans(local string) (err error) {
  // 修改 gin 中 validator 实现定制
  if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
    zhT := zh.New()
    enT := en.New()
    // 第一个参数是备用的语言环境,后面的参数是应该支持的语言环境
    uni := ut.New(enT, zhT, enT)
    trans, ok = uni.GetTranslator(local)
    if !ok {
      return fmt.Errorf("uni.GetTranslator(%s)", local)
    }
    switch local {
    case "en":
      en_translations.RegisterDefaultTranslations(v, trans)
    case "zh":
      zh_translations.RegisterDefaultTranslations(v, trans)
    default:
      en_translations.RegisterDefaultTranslations(v, trans)
    }
    return nil
  }
  return nil
}

然后在 main 函数中调用 InitTrans

func main() {
  // 调用 InitTrans
  if err := InitTrans("zh"); err != nil {
    fmt.Println("初始化翻译器错误")
    return
  }
  r := gin.Default()
  r.POST("/signup", func(c *gin.Context) {
    var signUpFrom SignUpForm
    if err := c.ShouldBindJSON(&signUpFrom); err != nil {
      // 判断 err 是否是 validator.ValidationErrors 类型
      errs, ok := err.(validator.ValidationErrors)
      if !ok {
        // 如果不是 validator.ValidationErrors 类型,返回原本的错误原因
        c.JSON(http.StatusBadRequest, gin.H{
          "error": err.Error(),
        })
        return
      }
      // 否则返回 validator.ValidationErrors 类型错误
      c.JSON(http.StatusBadRequest, gin.H{
        // 错误翻译
        "error": errs.Translate(trans),
      })
      return
    }
    c.JSON(http.StatusOK, gin.H{
      "message": "ok",
    })
  })
  r.Run()
}

这时我们拿到的错误信息是这样的:

{
  "error": {
    "SignUpForm.Email": "Email必须是一个有效的邮箱",
    "SignUpForm.RePassword": "RePassword必须等于Password"
  }
}

这种形式不是我们想要的结果,应该如何处理呢?

InitTrans 函数中,调用 RegisterTagNameFunc 方法,来读取 tag 中的 json 标签

v.RegisterTagNameFunc(func(fld reflect.StructField) string {
  name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
  // 如果 json tag 为 - 则不处理
  if name == "-" {
    return ""
  }
  return name
})

这时我们拿到的错误信息是这样的:

{
  "error": {
    "SignUpForm.email": "email必须是一个有效的邮箱",
    "SignUpForm.re_password": "re_password必须等于Password"
  }
}

我们还需要把 SignUpForm.email 转换成 email

func removeTopStruct(fields map[string]string) map[string]string {
  rsp := map[string]string{}
  for field, err := range fields {
    rsp[field[strings.Index(field, ".")+1:]] = err
  }
  return rsp
}

c.JSON(http.StatusBadRequest, gin.H{
  "error": removeTopStruct(errs.Translate(trans)),
})

这时我们拿到的错误信息是这样的:

{
  "error": {
    "email": "email必须是一个有效的邮箱",
    "re_password": "re_password必须等于Password"
  }
}

最终完整的代码是:

import (
  "github.com/gin-gonic/gin"
  "github.com/gin-gonic/gin/binding"
  "github.com/go-playground/locales/en"
  "github.com/go-playground/locales/zh"
  ut "github.com/go-playground/universal-translator"
  "github.com/go-playground/validator/v10"
  en_translations "github.com/go-playground/validator/v10/translations/en"
  zh_translations "github.com/go-playground/validator/v10/translations/zh"
)
var trans ut.Translator
// removeTopStruct 去除掉错误提示中的结构体名称
func removeTopStruct(fields map[string]string) map[string]string {
	rsp := map[string]string{}
	for field, err := range fields {
		rsp[field[strings.Index(field, ".")+1:]] = err
	}
	return rsp
}

func InitTrans(local string) (err error) {
	// 修改 gin 中 validator 实现定制
	if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
    // 将 json tag 作为字段名
		v.RegisterTagNameFunc(func(fld reflect.StructField) string {
			name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
			// 如果 json tag 为 - 则不处理
			if name == "-" {
				return ""
			}
			return name
		})

		zhT := zh.New()
		enT := en.New()
		// 第一个参数是备用的语言环境,后面的参数是应该支持的语言环境
		uni := ut.New(enT, zhT, enT)
		trans, ok = uni.GetTranslator(local)
		if !ok {
			return fmt.Errorf("uni.GetTranslator(%s)", local)
		}
		switch local {
		case "en":
			en_translations.RegisterDefaultTranslations(v, trans)
		case "zh":
			zh_translations.RegisterDefaultTranslations(v, trans)
		default:
			en_translations.RegisterDefaultTranslations(v, trans)

		}
		return
	}
	return
}

func main() {
  // 调用 InitTrans
	if err := InitTrans("zh"); err != nil {
		fmt.Println("初始化翻译器错误")
		return
	}
	r := gin.Default()

	r.POST("/signup", func(c *gin.Context) {
		var signUpFrom SignUpForm
		if err := c.ShouldBindJSON(&signUpFrom); err != nil {
      // 判断 err 是否是 validator.ValidationErrors 类型
			errs, ok := err.(validator.ValidationErrors)
			if !ok {
				c.JSON(http.StatusOK, gin.H{
					"error": err.Error(),
				})
			}
      // 否则返回 validator.ValidationErrors 类型错误
			c.JSON(http.StatusBadRequest, gin.H{
        // 删除掉错误提示中的结构体名称
				"error": removeTopStruct(errs.Translate(trans)),
			})
			return
		}
		c.JSON(http.StatusOK, gin.H{
			"message": "ok",
		})
	})
	r.Run()
}

中间件

全局生效:

router := gin.New()
router.Use(gin.Logger(), gin.Recovery())

分组使用:

router := gin.New()
v1 := router.Group("/v1")
v1.Use(gin.Logger(), gin.Recovery())

自定义中间件:

router := gin.New()
router.Use(MyLogger())

func MyLogger() gin.HandlerFunc{
  return func(c *gin.Context) {
    // 设置上下文需要使用的变量
    c.Set("example", "12345")
    // 请求前执行
    c.Next()
    // 请求后执行
  }
}

在使用中间件时会遇到一个问题,在 c.Next() 之前 return,之后的中间件还是会执行

如果解决这种情况?使用 c.Abort() 方法,这个方法会阻止之后的中间件执行

优雅的退出 gin 服务

当我们使用 ctrl + c 退出服务时,服务会立即退出,这样会导致一些问题,比如正在处理的请求会被中断,导致请求失败

我们可以用协程来解决这个问题

go func() {
  r.Run()
}()
quit := make(chan os.Signal)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit

// 处理后续逻辑
fmt.Println("shutdown server ...")

往期文章

转载自:https://juejin.cn/post/7256232552439267389
评论
请登录