How to unite two different struct using interface?
Asked Answered
O

2

5

I have the following code:

package main

import (
    "log"
)

type Data struct {
    Id int
    Name string
}

type DataError struct {
    Message string
    ErrorCode string
}

func main() {
    response := Data{Id: 100, Name: `Name`}
    if true {
        response = DataError{Message: `message`, ErrorCode: `code`}
    }
    log.Println(response)
}

This code returns me an error:

./start.go:20: cannot use DataError literal (type DataError) as type Data in assignment

Seems to be that I could not assign to response var data with different type (in my case DataError). I heard that possible solution could be to unite Data and DataError structs via interface. Or maybe there is another better solution?

Could you please point me how to resolve this problem?

Thanks

Outstare answered 27/9, 2016 at 12:52 Comment(4)
No, you can't assign different types to the same variable. You could an empty interface{} (play.golang.org/p/h5vPe3Et1O), but that's not an very good solution. In Go you usually use separate error values. Have you gone through any of the introductory materials?Firelock
@JimB, yes but interfaces still wasn't clear for me enough :)Outstare
There is no problem as there is no need to unite these two types.Pockmark
@tsg, as Volker said, there's no need to combine these, you can use separate values. Since neither type has any methods, the only method set that satisfies the same interface would be the empty interface{}.Firelock
E
7

It looks like you're trying to make a union type (what the ML-family of languages calls "enum"). I know of a couple of patterns for this:

0. Basic error handling (playground)

I suspect what you're doing is just basic error handling. In Go, we use multiple return values and check the result. This is almost certainly what you want to do:

package main

import (
    "fmt"
    "log"
)

type Data struct {
    ID   int
    Name string
}

type DataError struct {
    Message   string
    ErrorCode string
}

// Implement the `error` interface. `error` is an interface with
// a single `Error() string` method
func (err DataError) Error() string {
    return fmt.Sprintf("%s: %s", err.ErrorCode, err.Message)
}

func SomeFunction(returnData bool) (Data, error) {
    if returnData {
        return Data{ID: 42, Name: "Answer"}, nil
    }
    return Data{}, DataError{
        Message:   "A thing happened",
        ErrorCode: "Oops!",
    }
}

func main() {
    // this bool argument controls whether or not an error is returned
    data, err := SomeFunction(false)
    if err != nil {
        log.Fatalln(err)
    }
    fmt.Println(data)
}

1. Interfaces (playground)

Again, if your options are good-data and error, you should probably use the first case (stick with the idiom/convention), but other times you might have multiple "good data" options. We can use interfaces to solve this problem. Here we're adding a dummy method to tell the compiler to constrain the possible types that can implement this interface to those that have an IsResult() method. The biggest downside to this is that sticking things into an interface can incur an allocation, which can be detrimental in a tight loop. This pattern isn't terribly common.

package main

import "fmt"

type Result interface {
    // a dummy method to limit the possible types that can
    // satisfy this interface
    IsResult()
}

type Data struct {
    ID   int
    Name string
}

func (d Data) IsResult() {}

type DataError struct {
    Message   string
    ErrorCode string
}

func (err DataError) IsResult() {}

func SomeFunction(isGoodData bool) Result {
    if isGoodData {
        return Data{ID: 42, Name: "answer"}
    }
    return DataError{Message: "A thing happened", ErrorCode: "Oops!"}
}

func main() {
    fmt.Println(SomeFunction(true))
}

2. Tagged Union (playground)

This case is similar to the previous case, except instead of using an interface, we're using a struct with a tag that tells us which type of data the struct contains (this is similar to a tagged union in C, except the size of the struct is the sum of its potential types instead of the size of its largest potential type). While this takes up more space, it can easily be stack-allocated, thus making it tight-loop friendly (I've used this technique to reduce allocs from O(n) to O(1)). In this case, our tag is a bool because we only have two possible types (Data and DataError), but you could also use a C-like enum.

package main

import (
    "fmt"
)

type Data struct {
    ID   int
    Name string
}

type DataError struct {
    Message   string
    ErrorCode string
}

type Result struct {
    IsGoodData bool
    Data       Data
    Error      DataError
}

// Implements the `fmt.Stringer` interface; this is automatically
// detected and invoked by fmt.Println() and friends
func (r Result) String() string {
    if r.IsGoodData {
        return fmt.Sprint(r.Data)
    }
    return fmt.Sprint(r.Error)
}

func SomeFunction(isGoodData bool) Result {
    if isGoodData {
        return Result{
            IsGoodData: true,
            Data:       Data{ID: 42, Name: "Answer"},
        }
    }
    return Result{
        IsGoodData: false,
        Error: DataError{
            Message:   "A thing happened",
            ErrorCode: "Oops!",
        },
    }
}

func main() {
    // this bool argument controls whether or not an error is returned
    fmt.Println(SomeFunction(true))
}
Evy answered 27/9, 2016 at 15:24 Comment(2)
it's exactly what I needed! thanks a lot! Very detailed answer :)Outstare
@Outstare Happy to help. :)Evy
R
1

You can't assign 2 different types that are not "assignable" to the same variable ... unless you use a specific interface signature or empty interface.

https://golang.org/ref/spec#Assignability

that code would compile :

func main() {
    var response interface{} // empty interface AKA Object AKA void pointer
    response = Data{Id: 100, Name: `Name`}
    if true {
        response = DataError{Message: `message`, ErrorCode: `code`}
    }
    log.Println(response)
}

since every type implements empty interface, but you want to do that only if there is no other options.

if 2 types share some methods use a specific interface, for instance (pseudo-code) :

type Responder interface {
    Respond() string
}

type Data struct { /* code */
}

func (d Data) Respond() string { return "" }

type DataError struct { /* code */
}

func (d DataError) Respond() string { return "" }

func main() {

    var response Responder // declared as interface
    response = Data{}
    response = DataError{}
    fmt.Println(response)

}

Whenever you have doubts a quick scan of the go spec is useful, it is the only authority and pretty well written compared to most specs out there. For the most part the rules are crystal clear, and that's a strength of Go.

Requisite answered 27/9, 2016 at 14:6 Comment(1)
thanks for your quick response. It's very interesting for me :)Outstare

© 2022 - 2024 — McMap. All rights reserved.