gin 基本使用

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 ...")

往期文章

  1. go 项目ORM、测试、api文档搭建
  2. go 开发短网址服务笔记
  3. go 实现统一加载资源的入口
  4. go 语言编写简单的分布式系统
  5. go 中 rpc 和 grpc 的使用
  6. protocol 和 grpc 的基本使用
  7. go 基础知识
  8. grpc 的单向流和双向流
  9. GORM 基本使用

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYTqJMLc' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片