背景(大啰嗦)
在Java中我们可以比较容易地借助Spring框架等提供的注解来实现成员字段的值验证,另外Java也原生支持对成员字段赋默认值。然而在go语言中这些都不会原生支持,尤其是在前后端通过json数据交互时,会比较麻烦。
对于默认值,原生go语言有一个痛点:json反序列化场景中go基本类型的零值与空值无法区分(相关讨论:https://segmentfault.com/a/1190000044137766)。例如,string
字段零值是""
,无论json中该字段的值是null
还是""
,反序列化(json.Unmarshal
)之后都是""
,这就导致业务逻辑上无法区分这个值到底是不是上游指定的。当然,有一些骚操作可以避免这样的事情发生,比如将字段类型定义为*int
(指针),就有了默认值nil
,跟前端传入的0区分开了。然而实践中会使业务代码变得复杂易错。如果我们让其初值是一个业务上无意义的值,比如int=-1
,string="null"
,就可以避免零值和空值无法区分的问题了。一种通用方法是利用反射,结合tag功能实现默认值,该思路与github.com/mcuadros/go-defaults
不谋而合,那就没必要造轮子了。
对于校验器,go没有类似Java的@Valid
这样的注解。其实,可以自己为每个结构体实现一个Validate()
函数,每次使用值前先校验,然而这无疑是给程序员增加了负担,业务中有几十个接口,上百个结构体,一个一个写校验逻辑?疯了。最好是能有一个通用的校验器,每个结构体引用即可。正当我甩出键盘想实现一个通用的校验器,猛然发现著名go框架gin
的默认校验器github.com/go-playground/validator
满足了我对校验器的所有幻想。不过它也是有些小缺点的,下文讨论之。
本文记录一下在go中实现结构体字段赋默认值,结构体字段校验的方法。
结构体字段默认值
开源项目:https://github.com/mcuadros/go-defaults
使用前先安装:
go get github.com/mcuadros/go-defaults
使用示例1:
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的示例):
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)
}
输出如下:
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:
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
方法,也是比较难受的。暂时没有想到比较优雅的方法,如果有小伙伴有,欢迎指教~。下面提供一个可以简化的方法:
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及以下版本不会对结构体类型进行校验,如需开启,可以使用如下方法:
validate := validator.New(validator.WithRequiredStructEnabled())
这是官方文档开头就强调了的。不过v11版本将会默认开启,期待一下。
二、默认情况下,数组、map等嵌套类型不会进行校验,如需开启,需要加入dive
标签,参考https://pkg.go.dev/github.com/go-playground/validator/v10#hdr-Dive
综合使用
package main
import (
"bytes"
"encoding/json"
"fmt"
"github.com/go-playground/validator/v10"
"github.com/mcuadros/go-defaults"
)
type Track struct {
Display bool `json:"display" default:"true"`
NodeList []Node `json:"nodeList" validate:"required,dive"` // dive递归到元素进行验证,没有则Node验证不会生效
Tags []string `json:"tags" validate:"dive,required"` // dive后边表示对元素的验证
}
type Node struct {
Type string `json:"type" default:"video" validate:"oneof=audio video image mosaic text"`
Start float64 `json:"start" default:"0" validate:"min=0"`
End float64 `json:"end" default:"0" validate:"required_if=Type audio,required_if=Type video,gtfield=Start"`
Text string `json:"text" default:"" validate:"required_if=Type text"`
Hidden bool `json:"hidden" default:"false"`
}
// NewTrackFromJSON json反序列化场景使用该函数
func NewTrackFromJSON(data []byte) (*Track, error) {
track := &Track{}
defaults.SetDefaults(track) // 初始化默认值
_ = json.Unmarshal(data, track)
err := validator.New(validator.WithRequiredStructEnabled()).Struct(track) // 校验参数
if err != nil {
return track, err // 校验不通过
}
return track, nil
}
// SetDefaultAndValidate 已有结构体可以使用该函数设置默认值并验证
func (p *Track) SetDefaultAndValidate() error {
defaults.SetDefaults(p)
err := validator.New(validator.WithRequiredStructEnabled()).Struct(p) // 校验参数
if err != nil {
return err // 校验不通过
}
return nil
}
func (n *Node) UnmarshalJSON(data []byte) error {
type Alias Node
nAlias := (*Alias)(n)
defaults.SetDefaults(nAlias) // Node作为数组元素时,必须实现Unmarshal才能使用defaults
err := json.Unmarshal(data, nAlias) // Unmarshal不能传入n本身,会导致Unmarshal无限递归
if err != nil {
return err
}
return nil
}
var trackJSON = `{
"nodeList": [
{
"type": "wrong"
},
{
"type": "video"
},
{
"type": "text"
}
],
"tags": ["boy", ""]
}`
func main() {
track, err := NewTrackFromJSON([]byte(trackJSON))
if err != nil {
fmt.Println(err)
}
fmt.Println(string(ToJSONIndent(track, "", " ")))
}
func ToJSON(v any) []byte {
bf := bytes.NewBuffer([]byte{})
jsonEncoder := json.NewEncoder(bf)
jsonEncoder.SetEscapeHTML(false) // 防止<>&等html字符转义
_ = jsonEncoder.Encode(v)
return bf.Bytes()
}
func ToJSONIndent(v any, prefix, indent string) []byte {
data := ToJSON(v)
var out bytes.Buffer
_ = json.Indent(&out, data, prefix, indent)
return out.Bytes()
}
/* 输出:
Key: 'Track.NodeList[0].Type' Error:Field validation for 'Type' failed on the 'oneof' tag
Key: 'Track.NodeList[0].End' Error:Field validation for 'End' failed on the 'gtfield' tag
Key: 'Track.NodeList[1].End' Error:Field validation for 'End' failed on the 'required_if' tag
Key: 'Track.NodeList[2].End' Error:Field validation for 'End' failed on the 'gtfield' tag
Key: 'Track.NodeList[2].Text' Error:Field validation for 'Text' failed on the 'required_if' tag
Key: 'Track.Tags[1]' Error:Field validation for 'Tags[1]' failed on the 'required' tag
{
"display": true,
"nodeList": [
{
"type": "wrong",
"start": 0,
"end": 0,
"text": "",
"hidden": false
},
{
"type": "video",
"start": 0,
"end": 0,
"text": "",
"hidden": false
},
{
"type": "text",
"start": 0,
"end": 0,
"text": "",
"hidden": false
}
],
"tags": [
"boy",
""
]
}
*/
本站资源均来自互联网,仅供研究学习,禁止违法使用和商用,产生法律纠纷本站概不负责!如果侵犯了您的权益请与我们联系!
转载请注明出处: 免费源码网-免费的源码资源网站 » go结构体默认值和校验器(go-defaults、go-validator)
发表评论 取消回复