如何匹配字符串golang struct标签
How to match string golang struct tags
我有一个 json 带有动态键的数据,例如:
{
"id_employee": 123
}
或
{
"id_user": 321
}
我正在尝试将数据解组为结构。
在解组数据后,我如何创建结构标签以匹配示例中的所有这 2 个键“id_user”和“id_employee”?
interface User struct {
Id int64 .....
}
开始之前
一个小小的免责声明:我在脑海中写下了下面的所有代码片段,没有校对或任何类似的东西。代码尚未 copy-paste 就绪。这个答案的重点是为您提供一些方法,让您可以按照您的要求进行操作,解释为什么选择给定选项可能是个好主意,等等……第三种方法绝对是更好的方法,但是考虑到信息有限(没有关于您要解决的问题的具体信息),您可能需要进行更多挖掘才能找到最终解决方案。
接下来,我要问为什么你要尝试做这样的事情。如果您想要一种可用于解组不同有效负载的单一类型,我认为您正在引入 lot 的代码味道。如果有效负载不同,则它们 必须 代表不同的数据。想要将单个 catch-all 类型用于多个 data-sets IMO 只是自找麻烦。我将提供一些您可以可以执行此操作的方法,但我想在开始之前非常清楚这一点:
尽管这是可能的,但这不是一个好主意
一个较小的问题,但我必须指出:您包括这样的示例类型:
interface User struct {
Id int64
}
这是完全错误的。具有字段的结构不是接口,因此我将假设两件事向前发展。一种是您需要专门的用户类型,例如:
type Employee struct {
Id int64
}
type Employer struct {
Id int64
}
还有一个:
type User interface {
ID() int64
}
解组这些东西
因此,您可以通过多种方式完成您想要做的事情。凌乱但简单的方法是拥有一个包含字段所有可能排列的类型:
type AllUser struct {
UID int64 `json:"user_id"`
EID int64 `json:"employee_id"`
}
这可确保您的 JSON 输入中的两个 user_id
employee_id
字段都能找到一个家,并且会填充一个 ID 字段。但是,当您想实现 User
接口时,真正的混乱很快就会变得明显:
func (a AllUser) ID() int64 {
if a.UID != 0 {
return a.UID
}
if a.EID != 0 {
return a.EID
}
// and so on
return 0 // probably an error?
}
对于 getter 来说,这只是很多样板代码,但是 setters 呢?该字段可能尚未设置。您需要找出一种方法来从单个 setter 设置正确的 ID 字段。传入一个 enum/constant 来指定您要设置的字段,乍一看似乎是一种合理的方法,但仔细想想:它首先违背了拥有接口的目的。你会失去所有的抽象。所以这种做法是有很大缺陷的。
此外,如果您设置了员工 ID,则其他 ID 字段将默认为其 nil 值(0 表示 int64)。再次编组类型将导致 JSON 输出如下:
{
"employee_id": 123,
"user_id": 0,
"employer_id": 0,
}
您可以通过将类型更改为使用指针来解决此问题,并添加 omitempty
以跳过 JSON 输出中的 nil
字段:
type AllUser struct {
EID *int64 `json:"employee_id,omitempty"`
UID *int64 `json:"user_id,omitempty"`
}
同样,这是一件麻烦事,将导致您不得不在整个代码中处理指针字段(在不同的时间点可能为 nil,也可能不为 nil)。这并不难做到,但它增加了很多噪音,使代码更容易出现错误,并且只是 all-round 一个你应该尽可能避免的 PITA。而且你可以很容易地避免它。
自定义编组
更好的方法是创建一个嵌入 data-specific 类型的基类型。假设我们已经创建了 Employee
和 Employer
或 Customer
类型。这些类型都有一个 ID
字段,带有自己的标签,如下所示:
type Employee struct {
ID int64 `json:"employee_id"`
}
type FooUser struct {
ID int64 `json:"foo_id"`
}
接下来要做的是创建一个嵌入所有特定用户类型的 semi-generic 类型。可以在此基本类型上添加共享字段(例如,如果所有 data-sets 都有一个 name
字段)。接下来您要做的是将此复合类型嵌入到另一种实现自定义 marshal/unmarshalling 的类型中。这将允许您设置一些字段(就像我在此处的示例中包含的那样:例如,指定您正在处理的用户类型的字段)。
type UserType int
const (
EmployeeUserType UserType = iota
FooUserType
// go-style enum values for all user-types
)
type BaseUser struct {
WrappedUser
}
type WrappedUser struct {
*Employee // embed pointers to these types
*FooUser
Name string `json:"name"`
Type UserType `json:"-"` // ignore this in JSON unmarshalling
}
func (b *BaseUser) UnmarshalJSON(data []byte) error {
if err := json.Unmarshal(data, &b.WrappedUser); err != nil {
return err
}
if b.Employee != nil {
b.Type = EmployeeUserType // set the user-type flag
}
if b.FooUser != nil {
b.Type = FooUserType
}
return nil
}
func (b BaseUser) MarshalJSON() ([]byte, error) {
return json.Marshal(b.WrappedUser) // wrapped user doesn't have any custom handling
}
现在要实现 User
接口,您可以在 WrappedUser
类型上实现它(BaseUser
嵌入它,因此无论哪种方式都可以访问这些方法),并且您现在可以准确地知道您需要 get/set 哪些字段,因为您有类型标志可以告诉您:
func (w WrappedUser) ID() int64 {
switch w.Type {
case EmployeeUserType:
return w.Employee.ID
case FooUserType:
return w.FooUser.ID
}
return 0
}
同样可以用 setters:
func (w *WrappedUser) SetID(id int64) {
switch w.Type {
case EmployeeUserType:
if w.Employee == nil {
w.Employee = &Employee{}
}
w.Employee.ID = id
case FooUserType:
if w.FooUser == nil {
w.FooUser = &FooUser{}
}
w.FooUser.ID = id
}
}
使用像这样的自定义编组和嵌入类型稍微好一些,但正如您可能通过查看这个非常简单的示例可以看出的那样,它很快就会变得非常麻烦handle/maintain。
翻转脚本
现在我假设您希望能够将不同的有效负载解组为单一类型,因为很多字段是共享的,但 ID 字段等内容可能不同(user_id
vs employee_id
在这种情况下)。这是完全正常的。您问的是如何使用单个 catch-all 类型。这是一个 X-Y 问题。与其询问如何为所有特定 data-sets 使用单一类型,不如简单地为共享字段创建一个类型,然后依次将其包含到特定类型中它与自定义编组的方法非常相似,但简单了约 1,000,000 倍:
// BaseUser contains all fields all specific user-types share
type BaseUser struct {
Name string `json:"name"`
Active bool `json:"active"`
// etc...
}
// Employee is a user, that happens to be an employee
type Employee struct {
ID int64 `json:"employee_id"`
BaseUser // embed the other fields that all users share here
}
type FooUser struct {
ID int64 `json:"foo_id"`
BaseUser
Name string `json:"foo_user"` // override the name field of BaseUser
}
在BaseUser
类型上实现User
接口的所有方法,只在特定类型上实现IDgetter/setter,就大功告成了。如果您需要覆盖一个字段,就像我在 FooUser
类型上对 Name
所做的那样,那么您只需覆盖该字段上该单一类型的 getter/setter:
func (f FooUser) Name() string {
return f.Name
}
func (f *FooUser) SetName(n string) {
f.Name = n
}
这就是您需要做的全部。好,易于。您正在使用 JSON 数据。这意味着您正在从某个地方获取该数据(API,或者作为对某种数据存储的查询的响应)。如果您正在处理您请求的数据,您至少应该知道您期望什么样的响应数据。 API 是合同:我调用 X,服务以我请求的给定格式的数据或错误作为响应。我从商店查询 data-set Y,我要么得到了请求的数据,要么什么也没得到(可能会出错)。
如果您从文件或某些服务中提取数据,并且无法预测返回的内容,则需要修复 data-source。您不应该尝试围绕更基本的问题进行编码。必须的,我会花一些时间写一个小程序,例如,读取源文件,将它解组成像 map[string]interface{}
这样粗糙的东西,检查每个对象包含什么键,然后我会写将数据输出到不同的文件中,按类型分组,这样我就可以以更理智的方式摄取数据。
我有一个 json 带有动态键的数据,例如:
{
"id_employee": 123
}
或
{
"id_user": 321
}
我正在尝试将数据解组为结构。
在解组数据后,我如何创建结构标签以匹配示例中的所有这 2 个键“id_user”和“id_employee”?
interface User struct {
Id int64 .....
}
开始之前
一个小小的免责声明:我在脑海中写下了下面的所有代码片段,没有校对或任何类似的东西。代码尚未 copy-paste 就绪。这个答案的重点是为您提供一些方法,让您可以按照您的要求进行操作,解释为什么选择给定选项可能是个好主意,等等……第三种方法绝对是更好的方法,但是考虑到信息有限(没有关于您要解决的问题的具体信息),您可能需要进行更多挖掘才能找到最终解决方案。
接下来,我要问为什么你要尝试做这样的事情。如果您想要一种可用于解组不同有效负载的单一类型,我认为您正在引入 lot 的代码味道。如果有效负载不同,则它们 必须 代表不同的数据。想要将单个 catch-all 类型用于多个 data-sets IMO 只是自找麻烦。我将提供一些您可以可以执行此操作的方法,但我想在开始之前非常清楚这一点:
尽管这是可能的,但这不是一个好主意
一个较小的问题,但我必须指出:您包括这样的示例类型:
interface User struct {
Id int64
}
这是完全错误的。具有字段的结构不是接口,因此我将假设两件事向前发展。一种是您需要专门的用户类型,例如:
type Employee struct {
Id int64
}
type Employer struct {
Id int64
}
还有一个:
type User interface {
ID() int64
}
解组这些东西
因此,您可以通过多种方式完成您想要做的事情。凌乱但简单的方法是拥有一个包含字段所有可能排列的类型:
type AllUser struct {
UID int64 `json:"user_id"`
EID int64 `json:"employee_id"`
}
这可确保您的 JSON 输入中的两个 user_id
employee_id
字段都能找到一个家,并且会填充一个 ID 字段。但是,当您想实现 User
接口时,真正的混乱很快就会变得明显:
func (a AllUser) ID() int64 {
if a.UID != 0 {
return a.UID
}
if a.EID != 0 {
return a.EID
}
// and so on
return 0 // probably an error?
}
对于 getter 来说,这只是很多样板代码,但是 setters 呢?该字段可能尚未设置。您需要找出一种方法来从单个 setter 设置正确的 ID 字段。传入一个 enum/constant 来指定您要设置的字段,乍一看似乎是一种合理的方法,但仔细想想:它首先违背了拥有接口的目的。你会失去所有的抽象。所以这种做法是有很大缺陷的。
此外,如果您设置了员工 ID,则其他 ID 字段将默认为其 nil 值(0 表示 int64)。再次编组类型将导致 JSON 输出如下:
{
"employee_id": 123,
"user_id": 0,
"employer_id": 0,
}
您可以通过将类型更改为使用指针来解决此问题,并添加 omitempty
以跳过 JSON 输出中的 nil
字段:
type AllUser struct {
EID *int64 `json:"employee_id,omitempty"`
UID *int64 `json:"user_id,omitempty"`
}
同样,这是一件麻烦事,将导致您不得不在整个代码中处理指针字段(在不同的时间点可能为 nil,也可能不为 nil)。这并不难做到,但它增加了很多噪音,使代码更容易出现错误,并且只是 all-round 一个你应该尽可能避免的 PITA。而且你可以很容易地避免它。
自定义编组
更好的方法是创建一个嵌入 data-specific 类型的基类型。假设我们已经创建了 Employee
和 Employer
或 Customer
类型。这些类型都有一个 ID
字段,带有自己的标签,如下所示:
type Employee struct {
ID int64 `json:"employee_id"`
}
type FooUser struct {
ID int64 `json:"foo_id"`
}
接下来要做的是创建一个嵌入所有特定用户类型的 semi-generic 类型。可以在此基本类型上添加共享字段(例如,如果所有 data-sets 都有一个 name
字段)。接下来您要做的是将此复合类型嵌入到另一种实现自定义 marshal/unmarshalling 的类型中。这将允许您设置一些字段(就像我在此处的示例中包含的那样:例如,指定您正在处理的用户类型的字段)。
type UserType int
const (
EmployeeUserType UserType = iota
FooUserType
// go-style enum values for all user-types
)
type BaseUser struct {
WrappedUser
}
type WrappedUser struct {
*Employee // embed pointers to these types
*FooUser
Name string `json:"name"`
Type UserType `json:"-"` // ignore this in JSON unmarshalling
}
func (b *BaseUser) UnmarshalJSON(data []byte) error {
if err := json.Unmarshal(data, &b.WrappedUser); err != nil {
return err
}
if b.Employee != nil {
b.Type = EmployeeUserType // set the user-type flag
}
if b.FooUser != nil {
b.Type = FooUserType
}
return nil
}
func (b BaseUser) MarshalJSON() ([]byte, error) {
return json.Marshal(b.WrappedUser) // wrapped user doesn't have any custom handling
}
现在要实现 User
接口,您可以在 WrappedUser
类型上实现它(BaseUser
嵌入它,因此无论哪种方式都可以访问这些方法),并且您现在可以准确地知道您需要 get/set 哪些字段,因为您有类型标志可以告诉您:
func (w WrappedUser) ID() int64 {
switch w.Type {
case EmployeeUserType:
return w.Employee.ID
case FooUserType:
return w.FooUser.ID
}
return 0
}
同样可以用 setters:
func (w *WrappedUser) SetID(id int64) {
switch w.Type {
case EmployeeUserType:
if w.Employee == nil {
w.Employee = &Employee{}
}
w.Employee.ID = id
case FooUserType:
if w.FooUser == nil {
w.FooUser = &FooUser{}
}
w.FooUser.ID = id
}
}
使用像这样的自定义编组和嵌入类型稍微好一些,但正如您可能通过查看这个非常简单的示例可以看出的那样,它很快就会变得非常麻烦handle/maintain。
翻转脚本
现在我假设您希望能够将不同的有效负载解组为单一类型,因为很多字段是共享的,但 ID 字段等内容可能不同(user_id
vs employee_id
在这种情况下)。这是完全正常的。您问的是如何使用单个 catch-all 类型。这是一个 X-Y 问题。与其询问如何为所有特定 data-sets 使用单一类型,不如简单地为共享字段创建一个类型,然后依次将其包含到特定类型中它与自定义编组的方法非常相似,但简单了约 1,000,000 倍:
// BaseUser contains all fields all specific user-types share
type BaseUser struct {
Name string `json:"name"`
Active bool `json:"active"`
// etc...
}
// Employee is a user, that happens to be an employee
type Employee struct {
ID int64 `json:"employee_id"`
BaseUser // embed the other fields that all users share here
}
type FooUser struct {
ID int64 `json:"foo_id"`
BaseUser
Name string `json:"foo_user"` // override the name field of BaseUser
}
在BaseUser
类型上实现User
接口的所有方法,只在特定类型上实现IDgetter/setter,就大功告成了。如果您需要覆盖一个字段,就像我在 FooUser
类型上对 Name
所做的那样,那么您只需覆盖该字段上该单一类型的 getter/setter:
func (f FooUser) Name() string {
return f.Name
}
func (f *FooUser) SetName(n string) {
f.Name = n
}
这就是您需要做的全部。好,易于。您正在使用 JSON 数据。这意味着您正在从某个地方获取该数据(API,或者作为对某种数据存储的查询的响应)。如果您正在处理您请求的数据,您至少应该知道您期望什么样的响应数据。 API 是合同:我调用 X,服务以我请求的给定格式的数据或错误作为响应。我从商店查询 data-set Y,我要么得到了请求的数据,要么什么也没得到(可能会出错)。
如果您从文件或某些服务中提取数据,并且无法预测返回的内容,则需要修复 data-source。您不应该尝试围绕更基本的问题进行编码。必须的,我会花一些时间写一个小程序,例如,读取源文件,将它解组成像 map[string]interface{}
这样粗糙的东西,检查每个对象包含什么键,然后我会写将数据输出到不同的文件中,按类型分组,这样我就可以以更理智的方式摄取数据。