Home > Back-end >  How can I return two different concrete types from a single method in Go 1.18?
How can I return two different concrete types from a single method in Go 1.18?

Time:07-05

Let say that I have this code:

type Type1 struct {
    Name string `json:"name,omitempty"`
    Path string `json:"path"`
    File string `json:"file"`
    Tag  int    `json:"tag"`
    Num  int    `json:"num"`
}

func LoadConfiguration(data []byte) (*Type1, error) {
    config, err := loadConf1(data)
    if err != nil {
        return nil, err
    }
    confOther, err := loadConfOther1()
    if err != nil {
        return nil, err
    }
    // do something with confOther
    fmt.Println("confOther", confOther)
    if confOther.Tag == 0 {
        config.Num = 5
    }

    // do something with config attributes of type1
    if config.Tag == 0 {
        config.Tag = 5
    }
    if config.Num == 0 {
        config.Num = 4
    }

    return config, nil
}

func loadConf1(bytes []byte) (*Type1, error) {
    config := &Type1{}
    if err := json.Unmarshal(bytes, config); err != nil {
        return nil, fmt.Errorf("cannot load config: %v", err)
    }

    return config, nil
}

func loadConfOther1() (*Type1, error) {
    // return value of this specific type
    flatconfig := &Type1{}

    // read a file as []byte
    // written as a fixed array to simplify this example
    fileContent := []byte{10, 22, 33, 44, 55}

    if err := json.Unmarshal(fileContent, flatconfig); err != nil {
        return nil, fmt.Errorf("cannot read config %v", err)
    }

    return flatconfig, nil
}

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.

type Type2 struct {
    Name  string                  `json:"name,omitempty"`
    Path  string                  `json:"path"`
    Map   *map[string]interface{} `json:"map"`
    Other string                  `json:"other"`
}

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?

CodePudding user response:

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:

func LoadConfiguration(data []byte) (*Type1, error) {
    config := &Type1{}
    if err := loadConf(data, config); err != nil {
        return nil, err
    }
    // ...
}

// "magically" accepts any type
// you could actually get rid of the intermediate function altogether
func loadConf(bytes []byte, config any) error {
    if err := json.Unmarshal(bytes, config); err != nil {
        return fmt.Errorf("cannot load config: %v", err)
    }
    return nil
}

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

type Configurations interface {
    Type1 | Type2
}

func loadConf[T Configurations](bytes []byte) (*T, error) {
    config := new(T)
    if err := json.Unmarshal(bytes, config); err != nil {
        return nil, fmt.Errorf("cannot load config: %v", err)
    }
    return config, nil
}

func loadConfOther[T Configurations]() (*T, error) {
    flatconfig := new(T)
    // ... code
    return flatconfig, nil
}

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.

func LoadConfiguration1(data []byte) (*Type1, error) {
    config, err := loadConf[Type1](data)
    if err != nil {
        return nil, err
    }
    confOther, err := loadConfOther[Type1]()
    if err != nil {
        return nil, err
    }

    // ... type specific code

    return config, nil
}

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:

func LoadConfiguration[T Configuration](data []byte) (*T, error) {
    config, err := loadConf[T](data)
    if err != nil {
        return nil, err
    }
    // let's pretend there's only one value of type parameter type
    // type-specific code
    switch t := config.(type) {
        case *Type1:
            // ... some *Type1 specific code
        case *Type2:
            // ... some *Type2 specific code
        default:
            // 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
            panic("invalid type")
    }

    return config, nil
}

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

  • Related