Go Struct Tag and reflect

Go Struct Tag 是什么?如何透过 reflect 动态处理栏位?

前言

type Restaurant struct {
Name string
RestaurantId string `bson:"restaurant_id,omitempty"`
Cuisine string `bson:"cuisine,omitempty"`
Address interface{} `bson:"address,omitempty"`
Borough string `bson:"borough,omitempty"`
Grades []interface{} `bson:"grades,omitempty"`
}

最近在与 MongoDB 互动时发现 Struct 字段结尾有一段语法不是很熟悉,这篇文章探讨 Struct Tag 存在的原因以及解决什么问题。

Struct Tag

用于标示 Struct 字段的元数据(用于描述数据的资料)

Struct Tag 通常会在 runtime 执行时用作:「格式验证」、「数据序列化」、「数据库字段应对」……等用途,可用于存储任何数据没有限制。与其命令式的操纵数据,通过声明式的方式来描述数据格式是 Struct Tag 的主要用途。

用途范例
数据验证validate:"required,min=3"
字段权限auth:"admin_only"
序列化格式csv:"username"
对 ORM 的描述gorm:"primaryKey"
API export 控制expose:"false"

Struct Tag 格式

Go 标准对 struct tag 的格式 有惯例但非强制

key1:"value1" key2:"value2"

value 里常见分隔符:

  • , → 参数列表,例如:json:"name,omitempty"
  • : → key-value,例如:validate:"min=3"
  • | → 多条规则分隔,例如:rule:"a|b|c"(自定义风格)

Struct Tag 只是一串字符串用于描述字段,并不包含解析的逻辑,需要自己实践通过 reflect 官方套件。

reflect 套件操作

reflect 套件用于观察并修改值于 runtime

基于 Go 是静态类型的语言,在编译期就决定所有类型的语言,通过 relfect 套件读取 Struct Tag 并依其内容决定程序逻辑。

package main
import (
"fmt"
"reflect"
)
func ValidateRequired(s any) error {
v := reflect.ValueOf(s)
t := reflect.TypeOf(s)
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
tag := field.Tag.Get("validate")
if tag == "required" {
value := v.Field(i)
if value.IsZero() {
return fmt.Errorf("field '%s' is required", field.Name)
}
}
}
return nil
}
func main() {
type User struct {
Name string `validate:"required"`
}
u1 := User{Name: "John"}
fmt.Println(ValidateRequired(u1)) // <nil>
u2 := User{}
fmt.Println(ValidateRequired(u2)) // field 'Name' is required
}

总结

所以回到原先的问题,可以发现 MongoDB Driver for Go 使用 Struct Tags🔗 背后其实是为了将数据应对为 bson 才需要额外添加这些 struct tag。

延伸阅读