go结构体默认值和校验器(go-defaults、go-validator)

背景(大啰嗦)

在Java中我们可以比较容易地借助Spring框架等提供的注解来实现成员字段的值验证,另外Java也原生支持对成员字段赋默认值。然而在go语言中这些都不会原生支持,尤其是在前后端通过json数据交互时,会比较麻烦。

对于默认值,原生go语言有一个痛点:基本类型的零值可能跟业务值有冲突。例如,int字段默认值是0,有可能前端传入也是0,那么这个0到底是有效的还是无效的?换句话说,这个0到底是不是前端传入的?就不好判断了。当然,有一些骚操作可以避免这样的事情发生,比如将字段类型定义为*int(指针),就有了默认值nil,跟前端传入的0区分开了,但本文不探讨这种骚操作,实践中会多写很多判断逻辑且容易写错。比较容易想到的一种通用方法是利用反射,结合tag功能实现默认值,该思路与github.com/mcuadros/go-defaults不谋而合,那就没必要造轮子了,拿过来就用。

对于校验器,go没有类似Java的@Valid这样的注解。其实,可以自己为每个结构体实现一个Validate()函数,每次使用值前先校验,然而这无疑是给程序员增加了负担,业务中有几十个接口,上百个结构体,一个一个写校验逻辑?疯了。最好是能有一个通用的校验器,每个结构体引用即可。正当我甩出键盘想实现一个通用的校验器,猛然发现著名go框架gin的默认校验器github.com/go-playground/validator满足了我对校验器的所有幻想。不过它也是有些小缺点的,下文讨论之。

本文记录一下在go中实现结构体字段赋默认值,结构体字段校验的方法。

结构体字段默认值

开源项目:https://github.com/mcuadros/go-defaults

使用前先安装:

bash 复制代码
go get github.com/mcuadros/go-defaults

使用示例1:

go 复制代码
package main

import (
	"encoding/json"
	"fmt"
	"time"

	"github.com/mcuadros/go-defaults"
)

type Track struct {
	Name     string        `json:"name" default:"Unnamed"`
	Hidden   bool          `json:"hidden" default:"true"`
	Duration time.Duration `json:"duration" default:"1m1s1ms"`
}

var nodeJson = `{
	"name": "MyFirstTrack"
}`

func main() {
	track := new(Track)
	// 用default标签的默认值初始化
	defaults.SetDefaults(track)
	fmt.Printf("set default: %+v\n", track) // set default: &{Name:Unnamed Hidden:true Duration:1m1.001s}
	// 反序列化。比如json串可能是前端传过来的
	err := json.Unmarshal([]byte(nodeJson), track)
	if err != nil {
		fmt.Println("Unmarshal error.\n", err)
	}
	fmt.Printf("unmarshal: %+v\n", track) // unmarshal: &{Name:MyFirstTrack Hidden:true Duration:1m1.001s}
	// 再次尝试设置默认值,看是否生效
	defaults.SetDefaults(track)
	fmt.Printf("set default again: %+v\n", track) // set default: &{Name:MyFirstTrack Hidden:true Duration:1m1.001s}
}

分析:上面代码可以看出,defaults会将default标签指定的值赋值给结构体对象,但是,当字段已经有非零值时,defaults不再生效(这也是项目readme.md中警告过的)。

再来考虑,如果结构体字段为数组类型,defaults能生效吗?不妨验证一下。

使用示例2(这是一个有bug的示例):

go 复制代码
package main

import (
	"encoding/json"
	"fmt"
	"time"

	"github.com/mcuadros/go-defaults"
)

type Track struct {
	Name     string        `json:"name" default:"Unnamed"`
	Hidden   bool          `json:"hidden" default:"true"`
	Duration time.Duration `json:"duration" default:"1m1s1ms"`
	NodeList []struct {
		Start    time.Duration `json:"start" default:"1m"`
		Duration time.Duration `json:"duration" default:"2m2s2ms"`
	} `json:"nodeList"`
}

var nodeJson = `{
	"name": "MyFirstTrack",
	"hidden": false,
	"nodeList": [
		{
			"start": 0
		},
		{
			"duration": 1000000000
		}
	]
}`

func main() {
	track := new(Track)

	defaults.SetDefaults(track)
	fmt.Printf("set default: %+v\n", track)

	err := json.Unmarshal([]byte(nodeJson), track)
	if err != nil {
		fmt.Println("Unmarshal error.\n", err)
	}
	fmt.Printf("unmarshal: %+v\n", track)

	defaults.SetDefaults(track)
	fmt.Printf("set default again: %+v\n", track)
}

输出如下:

text 复制代码
set default: &{Name:Unnamed Hidden:true Duration:1m1.001s NodeList:[]}
unmarshal: &{Name:MyFirstTrack Hidden:false Duration:1m1.001s NodeList:[{Start:0s Duration:0s} {Start:0s Duration:1s}]}
set default again: &{Name:MyFirstTrack Hidden:true Duration:1m1.001s NodeList:[{Start:1m0s Duration:2m2.002s} {Start:1m0s Duration:1s}]}

分析:可以看出,对于数组字段,反序列化之前并不知道数组元素有几个,所以defaults不会初始化数组元素。而反序列化之后再次使用defaults,会将其中所有的零值修改为tag指定的默认值,比如hidden被错误的修改为了true。所以defaults和json反序列化同时存在时,无论谁先谁后,都不符合预期。

啊?难道defaults用不了了?其实需要一点点技巧规避一下这种问题(https://blog.csdn.net/qq_44336275/article/details/131436350也提到了这个问题)。不妨实现结构体的Unmarshal方法,使之在反序列化之前先调用一下defaults,再反序列化,即可避免数组元素无法赋默认值的问题。针对「使用示例2」,我们只需将NodeList元素单独定义并实现Unmarshal方法。

使用示例3:

go 复制代码
type Track struct {
	Name     string `json:"name" default:"Unnamed"`
	NodeList []Node `json:"nodeList"`
}

type Node struct {
	Type     TypeEnum `json:"type" default:"video"`
	Start    float64  `json:"start" default:"2"`
	Duration float64  `json:"duration" default:"5"`
}

func (n *Node) UnmarshalJSON(data []byte) error {
	type Alias Node
	n2 := new(Alias)
	defaults.SetDefaults(n2)
	err := json.Unmarshal(data, n2) // 务必传入Alias类型的n2,而不是n。传n会导致UnmarshalJSON无限递归调用
	if err != nil {
		return err
	}
	*n = Node(*n2)
	return nil
}

如此一来,满足了含数组字段的json反序列化场景的默认值初始化。

再进一步考虑,如果业务中有几十个结构体要作为数组元素(那估计是比较庞大的业务了),要为每一个结构体都实现Unmashal方法,也是比较难受的。暂时没有想到比较优雅的方法,如果有小伙伴有,欢迎指教~。下面提供一个可以简化的方法:

go 复制代码
func (n *Node) UnmarshalJSON(data []byte) error {
	type Alias Node
	return UnmarshalWithDefault[Node, Alias](data, n)
}

func UnmarshalWithDefault[T, TAlias any](data []byte, target *T) error {
	var aliasValue TAlias
	defaults.SetDefaults(&aliasValue)
	err := json.Unmarshal(data, &aliasValue)
	if err != nil {
		return err
	}
	targetReflect := reflect.ValueOf(target).Elem()  // 获得target的反射
	targetReflect.Set(reflect.ValueOf(aliasValue).Convert(targetReflect.Type())) // 将aliasValue转换赋值给target
	return nil
}

该方法封装了一个通用的UnmarshalWithDefault方法,以后在重写实现结构体Unmashal方法时只需调用即可。这里必须传入原类型的别名,是为了避免Unmarshal的无限递归调用。

【总结】

  • defaults 只会修改值为零值的字段为默认值;
  • json反序列化含有数组字段时,需要为数组元素结构体实现Unmarshal方法;

结构体校验器

开源项目:https://github.com/go-playground/validator

官方文档:https://pkg.go.dev/github.com/go-playground/validator/v10#readme-baked-in-validations

网络文章、官方文档已经提供了非常多且完善的示例,这里就不赘述了。不过有几个小问题最好注意一下:

一、默认情况下,v10及以下版本不会对结构体类型进行校验,如需开启,可以使用如下方法:

go 复制代码
validate := validator.New(validator.WithRequiredStructEnabled())

这是官方文档开头就强调了的。不过v11版本将会默认开启,期待一下。

二、默认情况下,数组、map等嵌套类型不会进行校验,如需开启,需要加入dive标签,参考https://pkg.go.dev/github.com/go-playground/validator/v10#hdr-Dive

相关推荐
Ajiang28247353042 小时前
对于C++中stack和queue的认识以及priority_queue的模拟实现
开发语言·c++
幽兰的天空2 小时前
Python 中的模式匹配:深入了解 match 语句
开发语言·python
Theodore_10225 小时前
4 设计模式原则之接口隔离原则
java·开发语言·设计模式·java-ee·接口隔离原则·javaee
----云烟----7 小时前
QT中QString类的各种使用
开发语言·qt
lsx2024067 小时前
SQL SELECT 语句:基础与进阶应用
开发语言
开心工作室_kaic7 小时前
ssm161基于web的资源共享平台的共享与开发+jsp(论文+源码)_kaic
java·开发语言·前端
向宇it7 小时前
【unity小技巧】unity 什么是反射?反射的作用?反射的使用场景?反射的缺点?常用的反射操作?反射常见示例
开发语言·游戏·unity·c#·游戏引擎
武子康7 小时前
Java-06 深入浅出 MyBatis - 一对一模型 SqlMapConfig 与 Mapper 详细讲解测试
java·开发语言·数据仓库·sql·mybatis·springboot·springcloud
qq_17448285758 小时前
springboot基于微信小程序的旧衣回收系统的设计与实现
spring boot·后端·微信小程序
转世成为计算机大神8 小时前
易考八股文之Java中的设计模式?
java·开发语言·设计模式