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
}
set cookie
net/http
: regarding Cookie
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
的相关方法
- Methods:
- Should bind - 实际开发中我们只要留意
ShouldBind
,ShouldBindJSON
这2个方法就好了- Methods:
ShouldBind
,ShouldBindJSON
,ShouldBindXML
,ShouldBindQuery
,ShouldBindYAML
- Behavior: 这些方法底层使用
ShouldBindWith
,如果存在绑定错误,则返回错误,开发人员可以正确的处理请求和错误
- Methods:
当我们使用绑定方法时,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来发送请求
测试登陆接口
- 不加任何参数
- 只传入user,不满足长度要求
- 只传入user,满足长度要求
- 传入user和password
测试注册接口
输入的字段只满足name
输入的字段满足所有要求
可以看到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也暴露出了定制自己的验证器的方法
- 定义验证器
//返回值是否通过验证
func ValidateXXX(fl validator.FieldLevel) bool{
//获取字段值
mobile := fl.Field().String()
//剩下的交给自己
return true
}
- 注册验证器
//注册验证器
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
_ = v.RegisterValidation("mobile", ValidateXXX)
}
- 接着我们就可以像下面这样添加自定义规则了
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!