Scott

gin quick start 2 years ago

go
29170个字符
共有133人围观

gin目前已有69.3K star

一个简单的http demo

package main

import (
  "github.com/gin-gonic/gin"
  "log"
)

func main() {
  //gin.SetMode(gin.ReleaseMode)
  router := gin.New()
  router.GET("/", index)
  log.Println("server is running at http://127.0.0.1:3333")
  router.Run(":3333")
}

func index(c *gin.Context) {
  //db operation
  c.JSON(200, gin.H{
    "code":  "2000",
    "msg":   "ok",
    "likes": []string{"apple", "pear", "orange"},
  })
}

gin重要struct

想要了解gin,必须掌握 gin.Context

type Context struct {
  Request *http.Request
  Writer  ResponseWriter

  Params Params

  // Keys is a key/value pair exclusively for the context of each request.
  Keys map[string]interface{}

  // Errors is a list of errors attached to all the 
  // handlers/middlewares who used this context.
  Errors errorMsgs

  // Accepted defines a list of manually accepted formats 
  // for content negotiation.
  Accepted []string
  // contains filtered or unexported fields
}

type ResponseWriter interface {
  http.ResponseWriter
  http.Hijacker
  http.Flusher
  http.CloseNotifier

  // Status returns the HTTP response status code of the current request.
  Status() int

  // Size returns the number of bytes 
  // already written into the response http body.
  // See Written()
  Size() int

  // WriteString writes the string into the response body.
  WriteString(string) (int, error)

  // Written returns true if the response body was already written.
  Written() bool

  // WriteHeaderNow forces to write the http header (status code + headers).
  WriteHeaderNow()

  // Pusher get the http.Pusher for server push
  Pusher() http.Pusher
}

net/http: regarding Cookie

http.Request

type Cookie struct {
  Name  string //eg:"x-my-token"
  Value string //jwt token

  Path       string    // optional eg:"/"
  Domain     string    // optional eg:"localhost"
  Expires    time.Time // optional
  RawExpires string    // for reading cookies only

  // MaxAge=0 means no 'Max-Age' attribute specified.
  // MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0'
  // MaxAge>0 means Max-Age attribute present and given in seconds
  MaxAge   int
  Secure   bool //需要设置成false 否则postman拿不到预期的response
  HttpOnly bool //需设置为true
  SameSite SameSite
  Raw      string
  Unparsed []string // Raw text of unparsed attribute-value pairs
}
func SetCookie(c *gin.Context) {
  cookie := http.Cookie{
    Name:       "x-token",
    Value:      "jwt_token_value",
    Path:       "/",
    Domain:     "localhost",
    Expires:    time.Now().Add(time.Minute * time.Duration(5)), //5 min 后过期
    RawExpires: "",
    MaxAge:     0,
    Secure:     false, //如果这里设置为true 则postman永远拿不到cookie
    HttpOnly:   true,
    SameSite:   0,
    Raw:        "",
    Unparsed:   nil,
  }
  http.SetCookie(c.Writer, &cookie)
  c.JSON(200, gin.H{
    "code":  2000,
    "msg":   "success",
  })
}

获取参数

package main

import (
  "github.com/gin-gonic/gin"
  "log"
)

func main() {
  route := gin.Default()

  apiGroup := route.Group("/api")
  {
    //http://127.0.0.1:9999/api/test1
    apiGroup.GET("/test1", Test1)
    //http://127.0.0.1:9999/api/test2                
    apiGroup.GET("/test2", Test2)
    //http://127.0.0.1:9999/api/test3                
    apiGroup.GET("/test3/:name/:company", Test3) 
  }

  log.Fatal(route.Run(":9999"))
}

func Test1(c *gin.Context) {
  //get username & password
  u := struct {
    Username string `json:"username"`
    Password string `json:"password"`
  }{}
  err := c.ShouldBindJSON(&u)
  if err != nil {
    c.JSONP(200, gin.H{
      "code": 2001,
      "msg":  "bad request",
    })
    return
  }

  c.JSONP(200, gin.H{
    "code": 2000,
    "msg":  "success",
    "data": u,
  })
}

func Test2(c *gin.Context) {
  //func (c *Context) Query(key string) (value string
  name := c.Query("name")
  age := c.Query("age")
  //建议使用DefaultQuery() 如果用户没输入还可设定默认值
  //注意Query()返回值都是string
  //name := C.DefaultQuery("name","Scott")
  //age := C.DefaultQuery("age","18")
  c.JSON(200, gin.H{
    "code": 2000,
    "name": name,
    "age":  age,
  })
}

func Test3(c *gin.Context) {
  //func (c *Context) Param(key string) string
  name := c.Param("name")
  company := c.Param("company")
  c.JSON(200, gin.H{
    "code":    2000,
    "name":    name,
    "company": company,
  })
}

如果前端是通过json传递数据的 那么需要调用 c.ShouldBindJSON()

如果是通过papram url传递 则调用c.Query()

如果是通过变量的方式传递 则调用c.Param()

middleware

gin的middleware很简单:定义一个function 返回 gin.HandlerFunc 即可

package main

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

func main() {
  route := gin.Default()

  apiGroup := route.Group("/api")
  apiGroup.Use(MiddlewareDemo())
  {
    apiGroup.GET("/test1", Test1) //http://127.0.0.1:9999/api/test1
    apiGroup.GET("/test2", Test2) //http://127.0.0.1:9999/api/test2
  }

  log.Fatal(route.Run(":9999"))
}

func Test1(c *gin.Context) {
  c.JSONP(http.StatusOK, gin.H{
    "code": 2000,
    "msg":  "success",
    "data": "test1",
  })
}

func Test2(c *gin.Context) {
  c.JSONP(http.StatusOK, gin.H{
    "code": 2000,
    "msg":  "success",
    "data": "test2",
  })
}

// gin的middleware很简单:定义一个function 返回 gin.HandlerFunc 即可
func MiddlewareDemo() gin.HandlerFunc {
  return func(c *gin.Context) {
    log.Println("middleware called")
    //拦截用c.Abort()
    c.Next()
  }
}

middleware执行顺序:

package main

import (
  "fmt"
  "github.com/gin-gonic/gin"
)

func main() {
  router := gin.Default()

  router.Use(globalMiddleware())

  router.GET("/rest/n/api/*some", mid1(), mid2(), handler)

  router.Run()
}

func globalMiddleware() gin.HandlerFunc {
  fmt.Println("globalMiddleware")
  //return前可以初始化 按注册顺序 只会执行一次
  return func(c *gin.Context) {
    fmt.Println("1")
    c.Next()
    fmt.Println("2")
  }
}

func handler(c *gin.Context) {
  fmt.Println("exec handler.")
}

func mid1() gin.HandlerFunc {
  fmt.Println("mid1")
  //return前可以初始化 按注册顺序 只会执行一次
  return func(c *gin.Context) {

    fmt.Println("3")
    c.Next()
    fmt.Println("4")
  }
}

func mid2() gin.HandlerFunc {
  fmt.Println("mid2")
  //return前可以初始化 按注册顺序 只会执行一次
  return func(c *gin.Context) {
    fmt.Println("5")
    c.Next()
    fmt.Println("6")
  }
}

启动服务:

请求:http://127.0.0.1:8080/rest/n/api/1

再次请求:http://127.0.0.1:8080/rest/n/api/1

画图表示:

参考官网: How to build one effective middleware?

常见middleware:

更多用法

Abort

package main

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

func main() {
  route := gin.Default()

  apiGroup := route.Group("/api").Use(AuthMiddleware())
  {
    apiGroup.GET("/test", Test)
  }

  log.Fatal(route.Run(":9999"))
}

func Test(c *gin.Context) {
  c.JSON(http.StatusUnauthorized, gin.H{
    "code": 2000,
    "msg":  "test",
  })
}
func AuthMiddleware() gin.HandlerFunc {
  return func(c *gin.Context) {
    isLogin := false
    if isLogin {
      c.Next()
    } else {
      c.JSON(http.StatusUnauthorized, gin.H{
        "code": 401,
        "msg":  "未登陆",
      })
      c.Abort() //Abort阻止了后续逻辑的执行 如果换成return是不行的 ,return并不能hold住后面的逻辑
    }
  }
}

Abort的原理是让Engine的index越界

render html

gin没有自己去实现一套模版的语法,它用的go语言标准库的一套模板html/template

web框架从请求到返回的全过程

API RELATED TO FILES

  • router.LoadHTMLFiles(file string)
  • router.LoadHTMLGlob("templates/*")
  • router.LoadHTMLGlob("templates/**/*")

以上与文件相关的的API不能同时出现,只能选一个, 完整demo

exit gracefully

package main

import (
  "fmt"
  "github.com/gin-gonic/gin"
  "net/http"
  "os"
  "os/signal"
  "syscall"
)

func main() {
  //优雅退出: 当我们关闭程序的时候应该做的后续处理,
  //比如我们做爬虫的时候,数据爬下来了,这时一个ctrl+c/kill程序关闭了 数据没有处理
  //微服务:启动之前 或者启动之后做一件事: 将当前的服务的ip地址和端口号注册到注册中心
  //我们当前的服务停止了以后并没有告知注册中心,这时如果前端正好请求了这个ip的服务 就会报错

  router := gin.Default()

  router.GET("/", func(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{
      "msg": "pong",
    })
  })

  //主协程退出 子协程也跟着退出
  go func() {
    router.Run(":8088")
  }()

  // 如果想要接收信号,kill可以,但kill -9 强杀命令 golang是没机会收到信号的
  quit := make(chan os.Signal)
  signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) //处理ctrl+c 和 kill
  <-quit

  //处理后续的逻辑
  fmt.Println("关闭server中。。。")
  fmt.Println("注销服务。。。")
}

表单验证

表单验证是web框架的一项重要功能

在gin中,若要将请求主体绑定到结构体中,需使用模型绑定,目前支持JSON, XML,YAML和标准表单值(foo=bar&boo=baz)的绑定

gin使用的是第三方库go-playground/validator来验证参数, GitHub Start 13.3K, documentation

使用方法:需要在绑定的字段上设置tag,比如,绑定格式为json,需这样设置json:"fieldname"

//postman/YAPI 
//如果想通过form-data表单的形式把数据传过来必须加 form:"user" 这个tag,
//否则只能通过raw/JSON的方式发送
type LoginForm struct {
  //多个条件用逗号隔开 不用加空格
  User     string `form:"user" json:"user" binding:"required,min=3,max=5"` 
  Password string `form:"user" json:"password" binding:"required"`
}

此外,gin还提供了2套绑定方法

  • Must Bind
    • Methods: Bind, BindJSON, BindXML, BindQuery, BindYAML
    • Behavior: 这些方法底层使用MustBindWith, 如果存在绑定错误,请求将被以下指令中止c.AbordWithError(400,err).SetType(ErrorTypeBind), 响应状态码会被设置为400,请求头Content-Type被设置为text/plain;charset=utf-8. 注意如果你试图在此之后设置响应代码,将会发出[Gin-debug] [Warning] Headers were already written. Wanted to override status code 400 with 422, 如果你希望更好的控制行为,请使用ShouldBind的相关方法
  • Should bind - 实际开发中我们只要留意ShouldBind, ShouldBindJSON这2个方法就好了
    • Methods: ShouldBind, ShouldBindJSON, ShouldBindXML, ShouldBindQuery, ShouldBindYAML
    • Behavior: 这些方法底层使用ShouldBindWith,如果存在绑定错误,则返回错误,开发人员可以正确的处理请求和错误

当我们使用绑定方法时,gin会根据Content-Type推断出使用哪种绑定器,如果你确定你绑定的是什么,可以使用MustBindWith或者BindingWith

你还可以给字段指定特定规则的修饰符,如果一个字段用binding:"required"修饰,并且在绑定改字段的值为空,那么将会返回一个错误

package main

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

// postman/YAPI
// 如果想通过form-data表单的形式把数据传过来必须加 form:"user" 这个tag,
// 否则只能通过raw/JSON的方式发送,一般我们配置json的tag就可以了
type LoginForm struct {
  //多个条件用逗号隔开 不用加空格
  User     string `form:"user" json:"user" binding:"required,min=3,max=5"`
  Password string `form:"password" json:"password" binding:"required"`
}

type SignUpParam struct {
  Name       string `json:"name" binding:"required"`
  Age        uint8  `json:"age" binding:"gte=1,lte=18"`
  Email      string `json:"email" binding:"required,email"`
  Password   string `json:"password" binding:"required"`
  RePassword string `json:"re_password" binding:"required,eqfield=Password"`
}

func main() {
  router := gin.Default()
  router.POST("/loginJSON", func(c *gin.Context) {
    var loginForm LoginForm
    if err := c.ShouldBind(&loginForm); err != nil {
      log.Println(err.Error())
      c.JSON(http.StatusBadRequest, gin.H{
        "error": err.Error(),
      })
      return
    }
    c.JSON(http.StatusOK, gin.H{
      "user":     loginForm.User,
      "password": loginForm.Password,
      "msg":      "登录成功",
    })
  })

  router.POST("/signup", func(c *gin.Context) {
    var signup SignUpParam
    if err := c.ShouldBind(&signup); err != nil {
      log.Println(err.Error())
      c.JSON(http.StatusBadRequest, gin.H{
        "error": err.Error(),
      })
      return
    }
    c.JSON(http.StatusOK, gin.H{
      "msg": "注册成功",
    })
  })
  router.Run(":8088")
}

接下来,我们用postman来发送请求

测试登陆接口

  1. 不加任何参数

  1. 只传入user,不满足长度要求

  1. 只传入user,满足长度要求

  1. 传入user和password

测试注册接口

输入的字段只满足name

输入的字段满足所有要求

:::tip 注意 可以看到gin会把验证的所有错误抛出来, 同时错误信息不太友好, 其实validator是支持多语言的,接下来我们将错误信息翻译成中文 :::

将错误信息翻译成中文

在看下面这个demo之前,请先读懂validator给的example

package main

import (
  "fmt"
  "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"
  "log"
  "net/http"
)

// postman/YAPI
//如果想通过form-data表单的形式把数据传过来必须加 form:"user" 这个tag,
//否则只能通过body/raw/JSON的方式发送
type SignUpForm struct {
  //多个条件用逗号隔开 不用加空格
  Name     string `json:"name" binding:"required,min=3"`
  Age      uint8  `json:"age" binding:"gte=1,lte=130"`
  Email    string `json:"email" binding:"required,email"`
  Password string `json:"password" binding:"required"`
  //跨字段
  RePassword string `json:"re_password" binding:"required,eqfield=Password"`
}

var trans ut.Translator //国际化翻译器

func InitTrans(locale 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(locale) //初始化全局翻译器
    if !ok {
      return fmt.Errorf("uni.GetTranslator(%s)", locale)
    }

    switch locale {
    case "en":
      en_translations.RegisterDefaultTranslations(v, trans)
    case "zh":
      zh_translations.RegisterDefaultTranslations(v, trans)
    default:
      en_translations.RegisterDefaultTranslations(v, trans)
    }
    return
  }
  return
}

func main() {
  if err := InitTrans("zh"); err != nil {
    fmt.Println("初始化翻译器错误")
    return
  }
  router := gin.Default()
  router.POST("/signpForm", func(c *gin.Context) {
    var signupForm SignUpForm
    if err := c.ShouldBind(&signupForm); err != nil {
      //
      errs, ok := err.(validator.ValidationErrors)
      //如果非验证错误
      if !ok {
        c.JSON(http.StatusOK, gin.H{
          "msg": errs.Error(),
        })
        return
      }
      //如果是验证错误
      c.JSON(http.StatusBadRequest, gin.H{
        "error": errs.Translate(trans),
      })
      return
      log.Println(err.Error())
      c.JSON(http.StatusBadRequest, gin.H{
        "error": err.Error(),
      })
      return
    }
    c.JSON(http.StatusOK, gin.H{
      "user":     signupForm.Name,
      "age":      signupForm.Age,
      "email":    signupForm.Email,
      "password": signupForm.Password,
      "msg":      "登录成功",
    })
  })
  router.Run(":8088")
}

接着我们用postman测试一下接口

可以看到错误信息提示已经人性化很多了,但是字段名还是golang的风格,不符合json的模式

将错误信息里的字段名大写改成小写

package main

import (
  "fmt"
  "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"
  "log"
  "net/http"
  "reflect"
  "strings"
)

// postman/YAPI
// 如果想通过form-data表单的形式把数据传过来必须加 form:"user" 这个tag,
// 否则只能通过body/raw/JSON的方式发送
type SignUpForm struct {
  //多个条件用逗号隔开 不用加空格
  Name     string `json:"name,aa" binding:"required,min=3"`
  Age      uint8  `json:"age" binding:"gte=1,lte=130"`
  Email    string `json:"email" binding:"required,email"`
  Password string `json:"password" binding:"required"`
  //跨字段
  RePassword string `json:"re_password" binding:"required,eqfield=Password"`
}

var trans ut.Translator //国际化翻译器

func InitTrans(locale string) (err error) {
  //修改gin框架中的validator引擎属性,实现定制
  if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
    //注册一个获取json的tag的自定义方法
    v.RegisterTagNameFunc(func(fld reflect.StructField) string {
      fmt.Println("fld.Tag.Get(json):", fld.Tag.Get("json"))
      name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
      fmt.Println("json tag name:", name)
      //兼容 - : golang中json tag为 - 会被忽略
      if name == "-" { //RePassword string `json:"-"`
        return ""
      }
      return name
    })
    zhT := zh.New()              //中文翻译器
    enT := en.New()              //英文翻译器
    //第一个参数是备用的语言环境 后面的参数是应该被支持的语言环境
    uni := ut.New(enT, zhT, enT) 
    trans, ok = uni.GetTranslator(locale)
    if !ok {
      return fmt.Errorf("uni.GetTranslator(%s)", locale)
    }

    switch locale {
    case "en":
      en_translations.RegisterDefaultTranslations(v, trans)
    case "zh":
      zh_translations.RegisterDefaultTranslations(v, trans)
    default:
      en_translations.RegisterDefaultTranslations(v, trans)
    }
    return
  }
  return
}

func main() {
  if err := InitTrans("zh"); err != nil {
    fmt.Println("初始化翻译器错误")
    return
  }
  router := gin.Default()
  router.POST("/signpForm", func(c *gin.Context) {
    var signupForm SignUpForm
    if err := c.ShouldBind(&signupForm); err != nil {
      //
      errs, ok := err.(validator.ValidationErrors)
      if !ok {
        c.JSON(http.StatusOK, gin.H{
          "msg": errs.Error(),
        })
        return
      }
      c.JSON(http.StatusBadRequest, gin.H{
        //errs.Translate(trans)的本质就是map[string]string
        "error": errs.Translate(trans), 
      })
      return
      log.Println(err.Error())
      c.JSON(http.StatusBadRequest, gin.H{
        "error": err.Error(),
      })
      return
    }
    c.JSON(http.StatusOK, gin.H{
      "user":     signupForm.Name,
      "age":      signupForm.Age,
      "email":    signupForm.Email,
      "password": signupForm.Password,
      "msg":      "登录成功",
    })
  })
  router.Run(":8088")
}

再次发送请求

可以看到 Email 已变成了email, 字段名已成功以json tag为主

最终改造

上面的实例还是不太友好,我们要将 "SignUpForm.email" 改成 "email"

errs.Translate(trans)的本质就是map[string]string,我们修改一下key就ok了

package main

import (
  "fmt"
  "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"
  "log"
  "net/http"
  "reflect"
  "strings"
)

// postman/YAPI
// 如果想通过form-data表单的形式把数据传过来必须加 form:"user" 这个tag,
// 否则只能通过body/raw/JSON的方式发送
type SignUpForm struct {
  //多个条件用逗号隔开 不用加空格
  Name     string `json:"name,aa" binding:"required,min=3"`
  Age      uint8  `json:"age" binding:"gte=1,lte=130"`
  Email    string `json:"email" binding:"required,email"`
  Password string `json:"password" binding:"required"`
  //跨字段
  RePassword string `json:"re_password" binding:"required,eqfield=Password"`
}

var trans ut.Translator //国际化翻译器

func InitTrans(locale string) (err error) {
  //修改gin框架中的validator引擎属性,实现定制
  if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
    //注册一个获取json的tag的自定义方法
    v.RegisterTagNameFunc(func(fld reflect.StructField) string {
      //fmt.Println("fld.Tag.Get(json):", fld.Tag.Get("json"))
      name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
      //fmt.Println("json tag name:", name)
      //兼容 - : golang中json tag为 - 会被忽略
      if name == "-" { //RePassword string `json:"-"`
        return ""
      }
      return name
    })
    zhT := zh.New() //中文翻译器
    enT := en.New() //英文翻译器
    //第一个参数是备用的语言环境 后面的参数是应该被支持的语言环境
    uni := ut.New(enT, zhT, enT)
    trans, ok = uni.GetTranslator(locale)
    if !ok {
      return fmt.Errorf("uni.GetTranslator(%s)", locale)
    }

    switch locale {
    case "en":
      en_translations.RegisterDefaultTranslations(v, trans)
    case "zh":
      zh_translations.RegisterDefaultTranslations(v, trans)
    default:
      en_translations.RegisterDefaultTranslations(v, trans)
    }
    return
  }
  return
}

// eg: "SignUpForm.email"` 改成 `"email"`
func fixStructKey(fileds map[string]string) map[string]string {
  rsp := make(map[string]string)
  for field, err := range fileds {
    rsp[field[strings.LastIndex(field, ".")+1:]] = err
  }
  return rsp
}

func main() {
  if err := InitTrans("zh"); err != nil {
    fmt.Println("初始化翻译器错误")
    return
  }
  router := gin.Default()
  router.POST("/signpForm", func(c *gin.Context) {
    var signupForm SignUpForm
    if err := c.ShouldBind(&signupForm); err != nil {
      //
      errs, ok := err.(validator.ValidationErrors)
      if !ok {
        c.JSON(http.StatusOK, gin.H{
          "msg": errs.Error(),
        })
        return
      }
      c.JSON(http.StatusBadRequest, gin.H{
        //errs.Translate(trans)的本质就是map[string]string
        "error": fixStructKey(errs.Translate(trans)),
      })
      return
      log.Println(err.Error())
      c.JSON(http.StatusBadRequest, gin.H{
        "error": err.Error(),
      })
      return
    }
    c.JSON(http.StatusOK, gin.H{
      "user":     signupForm.Name,
      "age":      signupForm.Age,
      "email":    signupForm.Email,
      "password": signupForm.Password,
      "msg":      "登录成功",
    })
  })
  router.Run(":8088")
}

再次发送请求

OK!

自定义验证器

业务要求变化多样,官方的validator不可能满足我们的所有要求,因此validator也暴露出了定制自己的验证器的方法

  1. 定义验证器
//返回值是否通过验证
func ValidateXXX(fl validator.FieldLevel) bool{ 
  //获取字段值
  mobile := fl.Field().String()
  //剩下的交给自己
  return true
}
  1. 注册验证器
//注册验证器
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
  _ = v.RegisterValidation("mobile", ValidateXXX)
}
  1. 接着我们就可以像下面这样添加自定义规则了
type LoginForm struct {
  //多个条件用逗号隔开 不能加空格
  Mobile   string `json:"mobile" form:"mobile" binding:"required,mobile"`
  Password string `json:"password" form:"password" binding:"required"`
}

来看看完整的demo:

package main

import (
  "fmt"
  "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"
  "net/http"
  "reflect"
  "regexp"
  "strings"
)

// postman/YAPI
// 如果想通过form-data表单的形式把数据传过来必须加 form:"user" 这个tag,
// 否则只能通过body/raw/JSON的方式发送
type LoginForm struct {
  //多个条件用逗号隔开 不用加空格
  Mobile   string `json:"mobile" form:"mobile" binding:"required,mobile"`
  Password string `json:"password" form:"password" binding:"required"`
}

var trans ut.Translator //国际化翻译器

// 返回值是否通过验证
func validateMobile(fl validator.FieldLevel) bool {
  //获取字段值
  mobile := fl.Field().String()
  //剩下的交给自己
  mobileRe := regexp.MustCompile(`^1([38][0-9]|14[579]|5[^4]|16[6]|7[1-35-8]|9[189])\d{8}$`)
  if res := mobileRe.FindString(mobile); len(res) > 0 {
    return true
  }
  return false
}

func InitTrans(locale string) (err error) {
  //修改gin框架中的validator引擎属性,实现定制
  if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
    //注册一个获取json的tag的自定义方法
    v.RegisterTagNameFunc(func(fld reflect.StructField) string {
      //fmt.Println("fld.Tag.Get(json):", fld.Tag.Get("json"))
      name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
      //fmt.Println("json tag name:", name)
      //兼容 - : golang中json tag为 - 会被忽略
      if name == "-" { //RePassword string `json:"-"`
        return ""
      }
      return name
    })
    zhT := zh.New() //中文翻译器
    enT := en.New() //英文翻译器
    //第一个参数是备用的语言环境 后面的参数是应该被支持的语言环境
    uni := ut.New(enT, zhT, enT)
    trans, ok = uni.GetTranslator(locale)
    if !ok {
      return fmt.Errorf("uni.GetTranslator(%s)", locale)
    }

    switch locale {
    case "en":
      en_translations.RegisterDefaultTranslations(v, trans)
    case "zh":
      zh_translations.RegisterDefaultTranslations(v, trans)
    default:
      en_translations.RegisterDefaultTranslations(v, trans)
    }
    return
  }
  return
}

// eg: "SignUpForm.email"` 改成 `"email"`
func fixStructKey(fileds map[string]string) map[string]string {
  rsp := make(map[string]string)
  for field, err := range fileds {
    rsp[field[strings.LastIndex(field, ".")+1:]] = err
  }
  return rsp
}

func handleValidatorError(c *gin.Context, err error) {
  errs, ok := err.(validator.ValidationErrors)
  if !ok {
    c.JSON(http.StatusOK, gin.H{
      "msg": errs.Error(),
    })
    return
  }
  c.JSON(http.StatusBadRequest, gin.H{
    //errs.Translate(trans)的本质就是map[string]string
    "error": fixStructKey(errs.Translate(trans)),
  })
  return
  //log.Println(err.Error())
  c.JSON(http.StatusBadRequest, gin.H{
    "error": err.Error(),
  })
}

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

  //注册验证器
  if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
    _ = v.RegisterValidation("mobile", validateMobile)
  }

  router := gin.Default()
  router.POST("/signpForm", func(c *gin.Context) {
    var loginForm LoginForm
    if err := c.ShouldBind(&loginForm); err != nil {
      handleValidatorError(c, err)
      return
    }
    c.JSON(http.StatusOK, gin.H{
      "msg": "登录成功",
    })
  })
  router.Run(":8088")
}

启动服务并向postman发起请求

  • a, 不携带任何参数, 接口返回正常

  • b, 手机号输入非法的

自定义的验证器居然漏翻译了

好在官方给出了解决方案(line 105-111)

package main

import (
  "fmt"
  "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"
  "net/http"
  "reflect"
  "regexp"
  "strings"
)

// postman/YAPI
// 如果想通过form-data表单的形式把数据传过来必须加 form:"user" 这个tag,
// 否则只能通过body/raw/JSON的方式发送
type LoginForm struct {
  //多个条件用逗号隔开 不用加空格
  Mobile   string `json:"mobile" form:"mobile" binding:"required,mobile"`
  Password string `json:"password" form:"password" binding:"required"`
}

var trans ut.Translator //国际化翻译器

// 返回值是否通过验证
func validateMobile(fl validator.FieldLevel) bool {
  //获取字段值
  mobile := fl.Field().String()
  //剩下的交给自己
  mobileRe := regexp.MustCompile(`^1([38][0-9]|14[579]|5[^4]|16[6]|7[1-35-8]|9[189])\d{8}$`)
  if res := mobileRe.FindString(mobile); len(res) > 0 {
    return true
  }
  return false
}

func InitTrans(locale string) (err error) {
  //修改gin框架中的validator引擎属性,实现定制
  if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
    //注册一个获取json的tag的自定义方法
    v.RegisterTagNameFunc(func(fld reflect.StructField) string {
      //fmt.Println("fld.Tag.Get(json):", fld.Tag.Get("json"))
      name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
      //fmt.Println("json tag name:", name)
      //兼容 - : golang中json tag为 - 会被忽略
      if name == "-" { //RePassword string `json:"-"`
        return ""
      }
      return name
    })
    zhT := zh.New() //中文翻译器
    enT := en.New() //英文翻译器
    //第一个参数是备用的语言环境 后面的参数是应该被支持的语言环境
    uni := ut.New(enT, zhT, enT)
    trans, ok = uni.GetTranslator(locale)
    if !ok {
      return fmt.Errorf("uni.GetTranslator(%s)", locale)
    }

    switch locale {
    case "en":
      en_translations.RegisterDefaultTranslations(v, trans)
    case "zh":
      zh_translations.RegisterDefaultTranslations(v, trans)
    default:
      en_translations.RegisterDefaultTranslations(v, trans)
    }
    return
  }
  return
}

// eg: "SignUpForm.email"` 改成 `"email"`
func fixStructKey(fileds map[string]string) map[string]string {
  rsp := make(map[string]string)
  for field, err := range fileds {
    rsp[field[strings.LastIndex(field, ".")+1:]] = err
  }
  return rsp
}

func handleValidatorError(c *gin.Context, err error) {
  errs, ok := err.(validator.ValidationErrors)
  if !ok {
    c.JSON(http.StatusOK, gin.H{
      "msg": errs.Error(),
    })
    return
  }
  c.JSON(http.StatusBadRequest, gin.H{
    //errs.Translate(trans)的本质就是map[string]string
    "error": fixStructKey(errs.Translate(trans)),
  })
  return
  //log.Println(err.Error())
  c.JSON(http.StatusBadRequest, gin.H{
    "error": err.Error(),
  })
}

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

  //注册验证器
  if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
    _ = v.RegisterValidation("mobile", validateMobile)
    _ = v.RegisterTranslation("mobile", trans, func(ut ut.Translator) error {
      return ut.Add("required", "{0}手机号码不合法", true) // see universal-translator for details
    }, func(ut ut.Translator, fe validator.FieldError) string {
      t, _ := ut.T("required", fe.Field())
      return t
    })
  }

  router := gin.Default()
  router.POST("/signpForm", func(c *gin.Context) {
    var loginForm LoginForm
    if err := c.ShouldBind(&loginForm); err != nil {
      handleValidatorError(c, err)
      return
    }
    c.JSON(http.StatusOK, gin.H{
      "msg": "登录成功",
    })
  })
  router.Run(":8088")
}

更新代码,再次请求

Everything is normal!