惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

IT之家
IT之家
H
Help Net Security
GbyAI
GbyAI
博客园_首页
G
Google Developers Blog
Microsoft Security Blog
Microsoft Security Blog
博客园 - 【当耐特】
月光博客
月光博客
美团技术团队
B
Blog RSS Feed
博客园 - 三生石上(FineUI控件)
WordPress大学
WordPress大学
博客园 - 叶小钗
有赞技术团队
有赞技术团队
T
The Blog of Author Tim Ferriss
Engineering at Meta
Engineering at Meta
Google DeepMind News
Google DeepMind News
Y
Y Combinator Blog
宝玉的分享
宝玉的分享
Microsoft Azure Blog
Microsoft Azure Blog
罗磊的独立博客
云风的 BLOG
云风的 BLOG
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
P
Proofpoint News Feed

郑文峰的博客

使用dify对接飞书多维表格 使用n8n对接飞书多维表格 服务启动时出现 OOM 一次服务升级时pg表DDL执行超时失败 Go语言高效IO缓冲技术详解 Go语言延迟初始化(Lazy Initialization)最佳实践 Go语言字符串拼接性能对比与优化指南 Go语言结构体内存对齐完全指南 Go语言空结构体:零内存消耗的高效编程 Go语言堆栈分配与逃逸分析深度解析 Go语言原子操作完全指南 Go语言内存预分配完全指南 Go语言不可变数据共享:无锁并发编程实践 Go语言零拷贝技术完全指南 Go语言遍历性能深度解析:从原理到优化实践 Go语言Interface Boxing原理与性能优化指南 Go协程池深度解析:原理、实现与最佳实践 使用etcd分布式锁导致的协程泄露与死锁问题 基于pre-commit的Python代码规范落地实践 初识 MCP Server pulsar阻塞导致logstash无法接入日志 django-prometheus使用及源码分析 kube-proxy源码分析 kubernetes service如何通过iptables转发 tcp缓存引起的日志丢失 django-apschedule定时任务异常停止 理解calico容器网络通信方案原理 理解flannel的三种容器网络方案原理 理解Linux IPIP隧道 理解VXLAN网络
优化gin表单的错误提示信息
zhengwenfeng · 2022-09-12 · via 郑文峰的博客

# 相关链接

gin官方例子 (opens new window)

文章的代码 (opens new window)

# 简单使用表单检验请求参数

创建一个简单的登录例子,我们对username和password绑定了required标签,代表着请求login接口的参数中必须包含这两个字段。

type User struct {
	UserName string `json:"username" binding:"required"`
	Password string `json:"password" binding:"required"`
}

func login(c *gin.Context) {

	var user User
	if err := c.ShouldBindJSON(&user); err != nil {
		c.JSON(http.StatusOK, gin.H{"msg": err.Error()})
		return
	}

	if user.UserName != "admin" || user.Password != "123456" {
		c.JSON(http.StatusUnauthorized, gin.H{"msg": "unauthorized"})
		return
	}

	c.JSON(http.StatusOK, gin.H{"msg": "you are logged in"})
}

func main() {
	r := gin.Default()
	r.POST("/login", login)
	r.Run() // 监听并在 0.0.0.0:8080 上启动服务
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

我们使用仅带有username去请求login接口,会输出如下,提示我们Password校验失败了,因为required的标签导致的。但是这个提示并不友好,我们需要进行优化展示。

{'msg': "Key: 'User.Password' Error:Field validation for 'Password' failed on the 'required' tag"}

1

# 翻译

我们需要对上面的提示信息进行一个翻译,并且可以支持各种语言的友好性提示。

我们在global/global.go文件中创建一个全局变量,该全局变量在后面的表单翻译中需要使用到

import ut "github.com/go-playground/universal-translator"

var (
	Trans ut.Translator
)

1
2
3
4
5

initialize/validator.go文件中编写内容如下,获取gin中的validate对象,然后给该对象绑定中文和英文的友好提示信息,我们可以通过locale来设置我们需要使用中文还是英文的信息。

func InitTrans(locale string) (err error) {

	if v, ok := binding.Validator.Engine().(*validator.Validate); ok {

		// 翻译
		zhT := zh.New()
		enT := en.New()
		uni := ut.New(enT, zhT, enT)

		global.Trans, ok = uni.GetTranslator(locale)
		if !ok {
			return fmt.Errorf("uni.GetTranslator(%s) error", locale)
		}

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

		return
	}
	return
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

最后在main.go中的main方法下调用上面的InitTrans方法来初始化翻译内容。

再将login方法中ShouldBindJSON返回的error转成validator.ValidationErrors类型,该类型包含一个Translate方法,调用该方法,再将之前的全局变量Trans传入。

func login(c *gin.Context) {

	var user User
	if err := c.ShouldBindJSON(&user); err != nil {

		errs, ok := err.(validator.ValidationErrors)
		if !ok {
			// 非校验错误,其他错误直接返回
			c.JSON(http.StatusOK, gin.H{"msg": err.Error()})
			return
		}

		c.JSON(http.StatusOK, gin.H{"msg": errs.Translate(global.Trans)})
		return
	}

	if user.UserName != "admin" || user.Password != "123456" {
		c.JSON(http.StatusUnauthorized, gin.H{"msg": "unauthorized"})
		return
	}

	c.JSON(http.StatusOK, gin.H{"msg": "you are logged in"})
}

func main() {
	err := initialize.InitTrans("zh")
	if err != nil {
		fmt.Printf("初始化翻译器错误, err = %s", err.Error())
		return
	}

	r := gin.Default()
	r.POST("/login", login)
	r.Run() // 监听并在 0.0.0.0:8080 上启动服务
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

我们再使用仅带有username字段去请求login接口,输出内容如下。

{'msg': {'User.Password': 'Password为必填字段'}}

1

但是,发现提示信息的key是User.Password,是表单对象和其字段名称,我们应该想要的是:

{'msg': {'password': 'Password为必填字段'}}

1

# 优化返回字段的key

我们修改InitTrans方法,通过go-playground提供的方法RegisterTagNameFunc来将我们自定义的方法注册进去,该自定义方法的目的是修改上面的Password改为json中的password,可以改成json标签中的值作为返回。

func InitTrans(locale string) (err error) {

	if v, ok := binding.Validator.Engine().(*validator.Validate); ok {

		//修改返回字段key的格式
		v.RegisterTagNameFunc(func(fld reflect.StructField) string {
			name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
			if name == "-" {
				return ""
			}
			return name
		})

		// 翻译
		zhT := zh.New()
		enT := en.New()
		uni := ut.New(enT, zhT, enT)

		global.Trans, ok = uni.GetTranslator(locale)
		if !ok {
			return fmt.Errorf("uni.GetTranslator(%s) error", locale)
		}

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

		return
	}
	return
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

再请求,响应如下,发现password已经改好了,但是User也想删除。

{'msg': {'User.password': 'password为必填字段'}}

1

我们在utils/validator.go文件中编写代码如下,该方法是用来删除User的。

func RemoveTopStruct(fields map[string]string) map[string]string {
	res := map[string]string{}
	for field, err := range fields {
		res[field[strings.Index(field, ".")+1:]] = err
	}
	return res
}

1
2
3
4
5
6
7
8

再在翻译返回的错误信息包上该方法。

func login(c *gin.Context) {

	var user User
	if err := c.ShouldBindJSON(&user); err != nil {

		errs, ok := err.(validator.ValidationErrors)
		if !ok {
			// 非校验错误,其他错误直接返回
			c.JSON(http.StatusOK, gin.H{"msg": err.Error()})
			return
		}

		c.JSON(http.StatusOK, gin.H{"msg": utils.RemoveTopStruct(errs.Translate(global.Trans))})
		return
	}

	if user.UserName != "admin" || user.Password != "123456" {
		c.JSON(http.StatusUnauthorized, gin.H{"msg": "unauthorized"})
		return
	}

	c.JSON(http.StatusOK, gin.H{"msg": "you are logged in"})
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

再执行,相应结果如下,这个就是我们想要的信息。

{'msg': {'password': 'password为必填字段'}}

1

# 总结

个人觉的虽然gin灵活小巧,但是功能真的很不完善。每次一次输出友好信息,我们都要手动调用Translate来翻译,并且还需要通过RemoveTopStruct方法来修改返回的信息,按简单的来说,应该由框架来做,我们只需要通过配置,就能自动输出我们想要的友好提示信息才对。