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

huangapple go评论82阅读模式
英文:

How to use generics in Unmarshal (go 1.18)

问题

我对golang的泛型还不太熟悉,但是我可以帮你翻译一下你的代码。以下是翻译好的内容:

我对golang的泛型还不太熟悉,并且有以下的设置。

  1. 我收集了各种不同类型的报告。
  2. 每个报告都有封闭字段。
  3. 所以我将其封装在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 如何在Unmarshal中使用泛型(go 1.18)

为了完整起见,我在这里添加了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.

  1. I've gathered loads of different kinds of reports.
  2. Each report has enclosing fields
  3. 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 如何在Unmarshal中使用泛型(go 1.18)

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 之前知道确切的类型

确切地说。

在编写代码时,必须知道实例化某些通用类型或函数(如 ReportContainerImplUnmarshalReportContainer)所需的具体类型。而 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 := &amp;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.

huangapple
  • 本文由 发表于 2022年4月21日 21:12:43
  • 转载请务必保留本文链接:https://go.coder-hub.com/71955113.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定