How can I return two different concrete types from a single method in Go 1.18?

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

How can I return two different concrete types from a single method in Go 1.18?

问题

让我们假设我有这段代码:

  1. type Type1 struct {
  2. Name string `json:"name,omitempty"`
  3. Path string `json:"path"`
  4. File string `json:"file"`
  5. Tag int `json:"tag"`
  6. Num int `json:"num"`
  7. }
  8. func LoadConfiguration(data []byte) (*Type1, error) {
  9. config, err := loadConf1(data)
  10. if err != nil {
  11. return nil, err
  12. }
  13. confOther, err := loadConfOther1()
  14. if err != nil {
  15. return nil, err
  16. }
  17. // 处理 confOther
  18. fmt.Println("confOther", confOther)
  19. if confOther.Tag == 0 {
  20. config.Num = 5
  21. }
  22. // 处理 Type1 的属性
  23. if config.Tag == 0 {
  24. config.Tag = 5
  25. }
  26. if config.Num == 0 {
  27. config.Num = 4
  28. }
  29. return config, nil
  30. }
  31. func loadConf1(bytes []byte) (*Type1, error) {
  32. config := &Type1{}
  33. if err := json.Unmarshal(bytes, config); err != nil {
  34. return nil, fmt.Errorf("无法加载配置:%v", err)
  35. }
  36. return config, nil
  37. }
  38. func loadConfOther1() (*Type1, error) {
  39. // 返回特定类型的值
  40. flatconfig := &Type1{}
  41. // 读取文件内容作为 []byte
  42. // 这里简化示例,将其写为固定数组
  43. fileContent := []byte{10, 22, 33, 44, 55}
  44. if err := json.Unmarshal(fileContent, flatconfig); err != nil {
  45. return nil, fmt.Errorf("无法读取配置:%v", err)
  46. }
  47. return flatconfig, nil
  48. }

唯一的公共函数是 LoadConfiguration

这段代码基于真实代码,用于将 JSON 数据读取为特定的结构体。如果有些部分看起来无用,那是因为我简化了原始代码。

上面的代码是可以工作的,但现在我想创建另一个名为 Type2 的结构体类型,并且在不复制和粘贴所有内容的情况下重用相同的方法来读取数据到 Type2 中。

  1. type Type2 struct {
  2. Name string `json:"name,omitempty"`
  3. Path string `json:"path"`
  4. Map *map[string]interface{} `json:"map"`
  5. Other string `json:"other"`
  6. }

基本上,我希望能够调用 LoadConfiguration 来获取 Type2。我可以接受调用一个特定的方法,比如 LoadConfiguration2,但我不想复制和粘贴 loadConf1loadConfOther1

在 Go 1.18 中,有没有一种符合惯用方式的方法来实现这个需求呢?

英文:

Let say that I have this code:

  1. type Type1 struct {
  2. Name string `json:"name,omitempty"`
  3. Path string `json:"path"`
  4. File string `json:"file"`
  5. Tag int `json:"tag"`
  6. Num int `json:"num"`
  7. }
  8. func LoadConfiguration(data []byte) (*Type1, error) {
  9. config, err := loadConf1(data)
  10. if err != nil {
  11. return nil, err
  12. }
  13. confOther, err := loadConfOther1()
  14. if err != nil {
  15. return nil, err
  16. }
  17. // do something with confOther
  18. fmt.Println("confOther", confOther)
  19. if confOther.Tag == 0 {
  20. config.Num = 5
  21. }
  22. // do something with config attributes of type1
  23. if config.Tag == 0 {
  24. config.Tag = 5
  25. }
  26. if config.Num == 0 {
  27. config.Num = 4
  28. }
  29. return config, nil
  30. }
  31. func loadConf1(bytes []byte) (*Type1, error) {
  32. config := &Type1{}
  33. if err := json.Unmarshal(bytes, config); err != nil {
  34. return nil, fmt.Errorf("cannot load config: %v", err)
  35. }
  36. return config, nil
  37. }
  38. func loadConfOther1() (*Type1, error) {
  39. // return value of this specific type
  40. flatconfig := &Type1{}
  41. // read a file as []byte
  42. // written as a fixed array to simplify this example
  43. fileContent := []byte{10, 22, 33, 44, 55}
  44. if err := json.Unmarshal(fileContent, flatconfig); err != nil {
  45. return nil, fmt.Errorf("cannot read config %v", err)
  46. }
  47. return flatconfig, nil
  48. }

The only public function is LoadConfiguration.

It's based on a real code and It's used to read a json data as a specific struct. If something seems useless, it's because I simplified the original code.

The code above is ok, but now I want to create another struct type called "Type2" and re-use the same methods to read data into Type2 without copying and pasting everything.

  1. type Type2 struct {
  2. Name string `json:"name,omitempty"`
  3. Path string `json:"path"`
  4. Map *map[string]interface{} `json:"map"`
  5. Other string `json:"other"`
  6. }

Basically, I want to be able to call LoadConfiguration to get also Type2. I can accept to call a specific method like LoadConfiguration2, but I don't want to copy and paste also loadConf1 and loadConfOther1.
Is there a way to do that in an idiomatic way in Go 1.18?

答案1

得分: 1

实际上,你在问题中展示的代码除了将一个类型传递给json.Unmarshal并格式化一个错误之外,没有做任何其他操作,所以你可以重写你的函数,使其行为与它完全相同:

  1. func LoadConfiguration(data []byte) (*Type1, error) {
  2. config := &Type1{}
  3. if err := loadConf(data, config); err != nil {
  4. return nil, err
  5. }
  6. // ...
  7. }
  8. // "神奇地"接受任何类型
  9. // 实际上,你可以完全摆脱中间函数
  10. func loadConf(bytes []byte, config any) error {
  11. if err := json.Unmarshal(bytes, config); err != nil {
  12. return fmt.Errorf("无法加载配置:%v", err)
  13. }
  14. return nil
  15. }

如果代码实际上比只是将指针传递给json.Unmarshal做更多的事情,那么它可以从类型参数中受益。

  1. type Configurations interface {
  2. Type1 | Type2
  3. }
  4. func loadConf[T Configurations](bytes []byte) (*T, error) {
  5. config := new(T)
  6. if err := json.Unmarshal(bytes, config); err != nil {
  7. return nil, fmt.Errorf("无法加载配置:%v", err)
  8. }
  9. return config, nil
  10. }
  11. func loadConfOther[T Configurations]() (*T, error) {
  12. flatconfig := new(T)
  13. // ... 代码
  14. return flatconfig, nil
  15. }

在这些情况下,你可以使用new(T)创建一个新的指针,然后json.Unmarshal将负责将字节切片或文件的内容反序列化到其中 - 前提是JSON实际上可以反序列化为任一结构体。

顶层函数中的特定类型代码仍然应该是不同的,特别是因为你想要使用显式具体类型实例化通用函数。所以我建议保留LoadConfiguration1LoadConfiguration2

  1. func LoadConfiguration1(data []byte) (*Type1, error) {
  2. config, err := loadConf[Type1](data)
  3. if err != nil {
  4. return nil, err
  5. }
  6. confOther, err := loadConfOther[Type1]()
  7. if err != nil {
  8. return nil, err
  9. }
  10. // ... 特定类型的代码
  11. return config, nil
  12. }

然而,如果特定类型的代码只是其中的一小部分,你可能可以通过类型切换来解决,尽管在你的情况下这似乎不是一个可行的选项。它可能如下所示:

  1. func LoadConfiguration[T Configuration](data []byte) (*T, error) {
  2. config, err := loadConf[T](data)
  3. if err != nil {
  4. return nil, err
  5. }
  6. // 假设只有一个类型参数类型的值
  7. // 特定类型的代码
  8. switch t := config.(type) {
  9. case *Type1:
  10. // ... 一些 *Type1 特定的代码
  11. case *Type2:
  12. // ... 一些 *Type2 特定的代码
  13. default:
  14. // 实际上不可能发生,因为 T 被限制为 Configuration,但如果你扩展了联合并忘记添加相应的 case,它有助于捕获错误
  15. panic("无效的类型")
  16. }
  17. return config, nil
  18. }

最小示例 playground:https://go.dev/play/p/-rhIgoxINTZ

英文:

Actually the code shown in your question doesn't do anything more than passing a type into json.Unmarshal and format an error so you can rewrite your function to behave just like it:

  1. func LoadConfiguration(data []byte) (*Type1, error) {
  2. config := &Type1{}
  3. if err := loadConf(data, config); err != nil {
  4. return nil, err
  5. }
  6. // ...
  7. }
  8. // "magically" accepts any type
  9. // you could actually get rid of the intermediate function altogether
  10. func loadConf(bytes []byte, config any) error {
  11. if err := json.Unmarshal(bytes, config); err != nil {
  12. return fmt.Errorf("cannot load config: %v", err)
  13. }
  14. return nil
  15. }

In case the code actually does something more than just passing a pointer into json.Unmarshal, it can benefit from type parameters.

  1. type Configurations interface {
  2. Type1 | Type2
  3. }
  4. func loadConf[T Configurations](bytes []byte) (*T, error) {
  5. config := new(T)
  6. if err := json.Unmarshal(bytes, config); err != nil {
  7. return nil, fmt.Errorf("cannot load config: %v", err)
  8. }
  9. return config, nil
  10. }
  11. func loadConfOther[T Configurations]() (*T, error) {
  12. flatconfig := new(T)
  13. // ... code
  14. return flatconfig, nil
  15. }

In these cases you can create a new pointer of either type with new(T) and then json.Unmarshal will take care of deserializing the content of the byte slice or file into it — provided the JSON can be actually unmarshalled into either struct.

The type-specific code in the top-level function should still be different, especially because you want to instantiate the generic functions with an explicit concrete type. So I advise to keep LoadConfiguration1 and LoadConfiguration2.

  1. func LoadConfiguration1(data []byte) (*Type1, error) {
  2. config, err := loadConf[Type1](data)
  3. if err != nil {
  4. return nil, err
  5. }
  6. confOther, err := loadConfOther[Type1]()
  7. if err != nil {
  8. return nil, err
  9. }
  10. // ... type specific code
  11. return config, nil
  12. }

However if the type-specific code is a small part of it, you can probably get away with a type-switch for the specific part, though it doesn't seem a viable option in your case. I would look like:

  1. func LoadConfiguration[T Configuration](data []byte) (*T, error) {
  2. config, err := loadConf[T](data)
  3. if err != nil {
  4. return nil, err
  5. }
  6. // let's pretend there's only one value of type parameter type
  7. // type-specific code
  8. switch t := config.(type) {
  9. case *Type1:
  10. // ... some *Type1 specific code
  11. case *Type2:
  12. // ... some *Type2 specific code
  13. default:
  14. // can't really happen because T is restricted to Configuration but helps catch errors if you extend the union and forget to add a corresponding case
  15. panic("invalid type")
  16. }
  17. return config, nil
  18. }

Minimal example playground: https://go.dev/play/p/-rhIgoxINTZ

huangapple
  • 本文由 发表于 2022年7月4日 16:05:48
  • 转载请务必保留本文链接:https://go.coder-hub.com/72853519.html
匿名

发表评论

匿名网友

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

确定