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

相关推荐
lsx20240614 分钟前
Django 视图详解
开发语言
h***066522 分钟前
【JSqlParser】Java使用JSqlParser解析SQL语句总结
java·开发语言·sql
代码or搬砖31 分钟前
Java Lambda 表达式全面详解
java·开发语言·python
这周也會开心41 分钟前
JDK1.8新增语法
java·开发语言
随风飘的云41 分钟前
es搜索引擎的持久化机制原理
后端
心随雨下43 分钟前
TypeScript泛型开发常见错误解析
java·开发语言·typescript
Se7en25811 小时前
基于 MateChat 构建 AI 编程智能助手的落地实践
后端
郝学胜-神的一滴1 小时前
现代OpenGL窗口管理:GLFW从入门到实战
开发语言·c++·程序人生·图形渲染·个人开发
n***F8751 小时前
Skywalking介绍,Skywalking 9.4 安装,SpringBoot集成Skywalking
spring boot·后端·skywalking
w***37512 小时前
SpringBoot【实用篇】- 测试
java·spring boot·后端