如何匹配字符串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 类型的基类型。假设我们已经创建了 EmployeeEmployerCustomer 类型。这些类型都有一个 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{} 这样粗糙的东西,检查每个对象包含什么键,然后我会写将数据输出到不同的文件中,按类型分组,这样我就可以以更理智的方式摄取数据。