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.New
和 gin.Default
都可以创建一个类型为 *gin.Engine
的 router
他们的区别是,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(¶ms); err != nil {
c.Status(http.StatusBadRequest)
return
}
c.JSON(http.StatusOK, gin.H{
"message": "id: " + id,
})
}
还有一种形式是使用 *
来匹配,比如 /goods/*name
如果访问的路由是 /goods/1/2/3/4
,那么 id
就是 1
,name
就是 /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-Type
是 application/x-www-form-urlencoded
和 application/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