英文:
How to use generics in Unmarshal (go 1.18)
问题
我对golang的泛型还不太熟悉,但是我可以帮你翻译一下你的代码。以下是翻译好的内容:
我对golang的泛型还不太熟悉,并且有以下的设置。
- 我收集了各种不同类型的报告。
- 每个报告都有封闭字段。
- 所以我将其封装在
ReportContainerImpl
中。
我使用了一个类型参数[T Reportable]
,其中Reportable
的定义如下:
type Reportable interface {
ExportDataPointReport | ImportDataPointReport | MissingDataPointReport | SensorThresoldReport
}
_type constraint_中的每个类型都是要嵌入在容器中的结构体。
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_也不支持指向“原始”泛型类型的指针。因此,我创建了一个ReportContainerImpl
实现的_interface。
type ReportContainer interface {
GetLocationID() string
GetProvider() string
GetReportType() ReportType
GetBody() interface{}
}
然后我遇到的问题是无法以任何形式对返回类型进行类型约束,而且在Unmarshal
完成后需要在GetBody()
函数上进行类型断言。
container, err := UnmarshalReportContainer(data)
if rep, ok := container.GetBody().(ExportDataPointReport); ok {
// 在这里使用ReportContainerImpl[ExportDataPointReport]...
}
也许我理解错了?- 但是无论如何,我总是需要一个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
// ...
}
}
英文:
I'm new to golang generics and have the following setup.
- I've gathered loads of different kinds of reports.
- Each report has enclosing fields
- So I wrapped it in a
ReportContainerImpl
I've used a type argument of [T Reportable]
where the Reportable
is defined as follows
type Reportable interface {
ExportDataPointReport | ImportDataPointReport | MissingDataPointReport | SensorThresoldReport
}
Each of the type in the type constraint is structs that is to be embedded in the container.
type ReportContainerImpl[T Reportable] struct {
LocationID string `json:"lid"`
Provider string `json:"pn"`
ReportType ReportType `json:"m"`
Body T `json:"body"`
}
I use a discriminator ReportType
to determine the concrete type when Unmarshal
.
type ReportType string
const (
ReportTypeExportDataPointReport ReportType = "ExportDataPointReport"
ReportTypeImportDataPointReport ReportType = "ImportDataPointReport"
ReportTypeMissingDataPointReport ReportType = "MissingDataPointReport"
ReportTypeSensorThresoldReport ReportType = "SensorThresoldReport"
)
Since go does not support type assertion for struct (only interfaces) it is not possible to cast the type when Unmarshal
. Also go does not support pointer to the "raw" generic type. Hence, I've created a interface that the ReportContainerImpl
implements.
type ReportContainer interface {
GetLocationID() string
GetProvider() string
GetReportType() ReportType
GetBody() interface{}
}
The problem I then get is that I cannot do type constrains on the return type in any form or shape and am back at "freetext semantics" on the GetBody()
function to allow for type assertion when Unmarshal
is done.
container, err := UnmarshalReportContainer(data)
if rep, ok := container.GetBody().(ExportDataPointReport); ok {
// Use the ReportContainerImpl[ExportDataPointReport] here...
}
Maybe I'm getting this wrong? - but however I do this, I always end up with somewhere needs a interface{}
or to know the exact type before Unmarshal
- Do you have a better suggestion how to solve this in a type (safer) way?
Cheers,
Mario
For completeness I add the UnmarshalReportContainer
here
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
// ...
}
}
答案1
得分: 10
> 但是,无论我怎么做,我总是最终需要一个 interface{} 或者在 Unmarshal 之前知道确切的类型
确切地说。
在编写代码时,必须知道实例化某些通用类型或函数(如 ReportContainerImpl
或 UnmarshalReportContainer
)所需的具体类型。而 JSON 反序列化是在运行时进行的,当你有一个填充了实际数据的字节切片时。
要根据某个区分值反序列化动态 JSON,仍然需要使用 switch
。
> 你有更好的建议如何以一种类型(更安全)的方式解决这个问题吗?
放弃参数化多态。它在这里不适用。保留你现在使用 json.RawMessage
的代码,在 switch
中有条件地反序列化动态数据,并返回实现 ReportContainer
接口的具体结构体。
<hr>
作为一种通用解决方案——仅当你能够克服这个先有鸡还是先有蛋的问题,并在编译时知道类型参数时,你可以编写一个最小的通用反序列化函数,如下所示:
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
中放入更多逻辑,那么使用它可能是合适的。具体情况可能有所不同;一般而言,当实际上不需要使用类型参数时,不要使用它们。
英文:
> but however I do this, I always end up with somewhere needs a interface{} or to know the exact type before Unmarshal
Precisely.
The concrete types needed to instantiate some generic type or function like ReportContainerImpl
or UnmarshalReportContainer
must be known at compile time, when you write the code. JSON unmarshalling instead occurs at run-time, when you have the byte slice populated with the actual data.
To unmarshal dynamic JSON based on some discriminatory value, you still need a switch
.
> Do you have a better suggestion how to solve this in a type (safer) way?
Just forgo parametric polymorphism. It's not a good fit here. Keep the code you have now with json.RawMessage
, unmarshal the dynamic data conditionally in the switch
and return the concrete structs that implement ReportContainer
interface.
<hr>
As a general solution — if, and only if, you can overcome this chicken-and-egg problem and make type parameters known at compile time, you can write a minimal generic unmarshal function like this:
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
}
This is only meant to illustrate the principle. Note that json.Unmarshal
already accepts any type, so if your generic function actually does nothing except new(T)
and return, like in my example, it is no different than "inlining" the entire thing as if unmarshalAny
didn't exist.
v, err := unmarshalAny[SomeType](src)
functionally equivalent as
out := &SomeType{}
err := json.Unmarshal(bytes, out)
If you plan to put more logic in unmarshalAny
, its usage may be warranted. Your mileage may vary; in general, don't use type parameters when it's not actually necessary.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论