如何在Unmarshal中使用泛型(Go 1.18)

10

我对golang泛型还不熟悉,目前有以下的设置。

  1. 我已经收集了许多不同类型的报告。
  2. 每个报告都有封闭字段
  3. 所以我将它包装在一个ReportContainerImpl

我使用了一个类型参数[T Reportable],其中Reportable的定义如下:

type Reportable interface {
    ExportDataPointReport | ImportDataPointReport | MissingDataPointReport | SensorThresoldReport
}

类型约束中的每个类型都是要嵌入到容器中的结构体。

type ReportContainerImpl[T Reportable] struct {
    LocationID string `json:"lid"`
    Provider string `json:"pn"`
    ReportType ReportType `json:"m"`
    Body T `json:"body"`
}

我使用一个鉴别器ReportType来确定在Unmarshal时具体的类型。

type ReportType string

const (
    ReportTypeExportDataPointReport ReportType = "ExportDataPointReport"
    ReportTypeImportDataPointReport ReportType = "ImportDataPointReport"
    ReportTypeMissingDataPointReport ReportType = "MissingDataPointReport"
    ReportTypeSensorThresoldReport ReportType = "SensorThresoldReport"
)

由于 go 不支持对 struct 进行类型断言(仅限 interfaces),因此在 Unmarshal 时无法进行类型转换。此外,go 不支持指向 "raw" 泛型类型的指针。因此,我创建了一个 interface,由 ReportContainerImpl 实现。

type ReportContainer interface {
    GetLocationID() string
    GetProvider() string
    GetReportType() ReportType
    GetBody() interface{}
}

我遇到的问题是,无论如何都不能对返回类型进行类型约束,在GetBody()函数上回到"自由文本语义",以允许在完成Unmarshal时进行类型断言。
    container, err := UnmarshalReportContainer(data)

    if rep, ok := container.GetBody().(ExportDataPointReport); ok {
      // Use the ReportContainerImpl[ExportDataPointReport] here...
    }

也许我理解有误?但是无论如何,我总是需要一个 interface{} 或在 Unmarshal 之前知道 确切的 类型才能完成某些操作。
  • 你有更好的建议以更安全的方式解决这个问题吗?

谢谢, Mario :)

为了完整起见,我在这里添加了 UnmarshalReportContainer

func UnmarshalReportContainer(data []byte) (ReportContainer, error) {

    type Temp struct {
        LocationID string `json:"lid"`
        Provider string `json:"pn"`
        ReportType ReportType `json:"m"`
        Body *json.RawMessage `json:"body"`
    }

    var temp Temp
    err := json.Unmarshal(data, &temp)
    if err != nil {
        return nil, err
    }

    switch temp.ReportType {
    case ReportTypeExportDataPointReport:
        var report ExportDataPointReport
        err := json.Unmarshal(*temp.Body, &report)
        return &ReportContainerImpl[ExportDataPointReport]{
            LocationID: temp.LocationID,
            Provider:   temp.Provider,
            ReportType: temp.ReportType,
            Body:       report,
        }, err

      // ...
    }
}

2
这看起来像是泛型的误用。泛型并不能替代接口的使用:何时使用泛型可能会提供一些有用的指导。 - JimB
1个回答

10
然而,无论我如何做,最终都需要一个interface{}或在Unmarshal之前知道确切的类型。
确切地说,必须在编写代码时在编译时知道一些通用类型或函数(如ReportContainerImpl或UnmarshalReportContainer)所需的具体类型。相反,在运行时发生JSON unmarshalling,此时有实际数据填充的字节片。
要根据某些磁盘值解析动态JSON,仍需要switch。
您是否有更好的建议以更安全的方式解决这个问题?
只需放弃参数多态性。它不适合这里。使用json.RawMessage保留您现在拥有的代码,在switch中有条件地解组动态数据,并返回实现ReportContainer接口的具体结构。
作为一般解决方案,如果且仅当您可以克服这个先有鸡还是先有蛋的问题并在编译时使类型参数已知,则可以编写最小化的通用解组函数,如下所示:
func unmarshalAny[T any](bytes []byte) (*T, error) {
    out := new(T)
    if err := json.Unmarshal(bytes, out); err != nil {
        return nil, err
    }
    return out, nil
}

这仅旨在说明原理。请注意,json.Unmarshal已经接受任何类型,因此,如果您的通用函数实际上除了像我的示例中那样执行new(T)和返回之外,什么也不做,那么它与完全“内联”整个内容没有区别,就好像unmarshalAny不存在一样。

v, err := unmarshalAny[SomeType](src)

在功能上等同于

out := &SomeType{}
err := json.Unmarshal(bytes, out)

如果您打算在unmarshalAny中加入更多的逻辑,则可能需要使用它。具体情况因人而异;通常情况下,不必要时不要使用类型参数。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接