1 背景
gin框架中使用到了validator包,但是没有自定义通义的错误信息的地方。故需要通过反射自行实现。
2 知识点
- golang的反射,struct的反射才能拿到tag、pointer\array\slice\map需要Elem()才能取到其元素类型。
- validator验证错误之后,err的结构。
3 目标
迭代判断、替换验证错误信息为自定义的错误信息。
4 源码
- 源码
package main
import (
"fmt"
"net/http"
"reflect"
"regexp"
"strings"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
)
// 定义请求参数结构体
type HelloParams struct {
Name string `json:"name" binding:"required" msg:"姓名不符合要求"`
GirlFriend []struct {
Name string `json:"name" binding:"required"`
Age int `json:"age" binding:"required,min=18" msg:"年龄需在18岁以上"`
} `json:"girl_friend" binding:"required,dive"`
Hourse map[string]struct {
Price int `json:"price" binding:"min=400000" msg:"价值需在40万以上"`
} `json:"hourse" binding:"required,dive"`
Hobby *struct {
Type string `json:"type" binding:"required" msg:"请填写爱好类型"`
Live struct {
Level string `json:"level" binding:"required" msg:"请为你的生活打分"`
}
} `json:"hobby" binding:"required"`
Food []string `json:"food" binding:"dive,min=3" msg:"食物3字符以上"`
}
func main() {
r := gin.Default()
r.POST("/hello", func(c *gin.Context) {
var params HelloParams
if err := c.ShouldBindJSON(¶ms); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"err": ParseError(params, err)})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Hello " + params.Name})
})
r.Run(":8080")
}
// 处理错误信息
func ParseError(obj any, err error) []string {
outMsg := []string{}
errMsg := make(map[string]string, 1)
if _, ok := err.(validator.ValidationErrors); !ok {
// 普通错误:直接返回信息
outMsg = append(outMsg, err.Error())
return outMsg
} else {
// binding类型的错误
for _, e := range err.(validator.ValidationErrors) {
errMsg[ParseKeyInDefaultErrMsg(e.Error())] = e.Error()
}
}
fmt.Printf("%#v\n", errMsg)
objType := reflect.TypeOf(obj)
ConvertErrMsgToTag(objType.Name(), objType, "", &errMsg, &outMsg)
return outMsg
}
// 从默认验证错误信息中,提取键路径
func ParseKeyInDefaultErrMsg(msg string) string {
reg, _ := regexp.Compile(`\[.+\]`)
l := make([]string, 0, 2)
msgTmp := strings.Split(msg, " ")
msg = strings.Trim(msgTmp[1], "'")
msgTmp = strings.Split(msg, ".")
for _, k := range msgTmp {
l = append(l, reg.ReplaceAllString(k, ""))
}
return strings.Join(l, ".")
}
// 将错误信息转换成对应的tag标识
//
// kPath: 键路径,迭代进入下一层时,传入已经过的key,以匹配对应的tag;
// objType: 反射目标;
// msgTag: 字段tag中的msg;
// errMsg: 解析后的错误信息;
// outMsg: 待返回的错误信息集;
func ConvertErrMsgToTag(kPath string, objType reflect.Type, msgTag string, errMsg *map[string]string, outMsg *[]string) {
_, haveErr := (*errMsg)[kPath]
switch objType.Kind() {
case reflect.Struct:
for i := 0; i < objType.NumField(); i++ {
f := objType.Field(i)
msg := f.Tag.Get("msg") // 结构体成员才有tag
ConvertErrMsgToTag(kPath+"."+f.Name, f.Type, msg, errMsg, outMsg)
}
case reflect.Array:
fallthrough
case reflect.Slice:
fallthrough
case reflect.Map:
fallthrough
case reflect.Pointer:
// array/slice/map,Elem()返回其成员类型;pointer,返回所指目标类型
// 此处prefix无需拼接subObj.Name(),仅是将Pointer类型转换类型,键路径未发生改变
subObj := objType.Elem()
if haveErr {
if msgTag != "" {
*outMsg = append(*outMsg, msgTag)
} else {
*outMsg = append(*outMsg, (*errMsg)[kPath])
}
} else {
ConvertErrMsgToTag(kPath, subObj, msgTag, errMsg, outMsg)
}
default: // 普通类型
if haveErr {
if msgTag != "" {
*outMsg = append(*outMsg, msgTag)
} else {
*outMsg = append(*outMsg, (*errMsg)[kPath])
}
}
}
}
- 测试用例
{
"name": "ted",
"girl_friend": [
{
"name": "小红",
"age": 17
},
{
"name": "小绿",
"age": 19
}
],
"hourse": {
"广州": {
"price": 39999
}
},
"hobby":{},
"food": ["披萨"]
}