Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Call Go function that accepts a slice of interface A with a slice of struct B (B implements A)

I have the following types:

type Statement interface {
    Say() string
}

type Quote struct {
    quote string
}

func (p Quote) Say() string {
    return p.quote
}

func Replay(conversation []Statement) {
    for _, statement := range conversation {
        fmt.Println(statement.Say())
    }
}

I think I have a fairly good grasp of why a function that accepts a parameter of type []Statement, cannot be called with []Quote; even though Quote implements Statement, []Quote does not implement []Statement. []Statement is not even an interface. It has the type slice of Statement. While Go implicitly converts from a type to an interface type, it does no implicit conversion from a slice of type A to a slice of interface B.

We can convert the quotes to statements explicitly:

conversation := []Quote{
    Quote{"Nice Guy Eddie: C'mon, throw in a buck!"},
    Quote{"Mr. Pink: Uh-uh, I don't tip."},
    Quote{"Nice Guy Eddie: You don't tip?"},
    Quote{"Mr. Pink: Nah, I don't believe in it."},
    Quote{"Nice Guy Eddie: You don't believe in tipping?"},
}

// This doesn't work
// Replay(conversation)

// Create statements from quotes
statements := make([]Statement, len(conversation))
for i, quote := range conversation {
    statements[i] = quote
}

Replay(statements)

Now say that Replay is part of a library that wants to go out of its way in how easy it's to use Replay. It allows you to call Replay with any slice of objects as long as those objects implement the Statement interface. To do so it has the following conversion method:

func ConvertToStatements(its interface{}) ([]Statement, error) {
    itsValue := reflect.ValueOf(its)
    itsKind := itsValue.Kind()
    if itsKind != reflect.Array && itsKind != reflect.Slice {
        return nil, fmt.Errorf("Expected items to be an Array or a Slice, got %s", itsKind)
    }
    itsLength := itsValue.Len()
    items := make([]Statement, itsLength)
    for i := 0; i < itsLength; i++ {
        itsItem := itsValue.Index(i)
        if item, ok := itsItem.Interface().(Statement); ok {
            items[i] = item
        } else {
            return nil, fmt.Errorf("item #%d does not implement the Statement interface: %s", i, itsItem)
        }
    }
    return items, nil
}

Replay looks like this:

func Replay(its interface{}) {
    conversation := ConvertToStatements(its)
    for _, statement := range conversation {
        fmt.Println(statement.Say())
    }
}

We can now call Replay with quotes directly:

Replay(conversation)

Finally, my question: Is there a simpler way to allow Replay to accept a slice of any type A, as long as A implements the Statement interface?

like image 245
Robert Kajic Avatar asked Nov 23 '13 14:11

Robert Kajic


People also ask

Can a struct have an interface Golang?

Like a struct an interface is created using the type keyword, followed by a name and the keyword interface . But instead of defining fields, we define a “method set”. A method set is a list of methods that a type must have in order to “implement” the interface.

Is slice passed by reference Golang?

Go slice is reference type A slice is a reference type in Go. This means that when we assign a reference to a new variable or pass a slice to a function, the reference to the slice is copied. In the code example, we define a slice and assign the slice to a new variable.

What is slice type Golang?

In Go language slice is more powerful, flexible, convenient than an array, and is a lightweight data structure. Slice is a variable-length sequence which stores elements of a similar type, you are not allowed to store different type of elements in the same slice.

How do I add a slice in Golang?

To add an element to a slice , you can use Golang's built-in append method. append adds elements from the end of the slice. The first parameter to the append method is a slice of type T . Any additional parameters are taken as the values to add to the given slice .


3 Answers

The following code has two different structure types that both implement the Say() function. You can create an array containing both types and call Replay()and have it do what you want:

package main

import "fmt"

type Statement interface {
    Say() string
}
type Statements []Statement

type Quote struct {
    quote string
}
type Quotes []Quote

func (p Quote) Say() string {
    return p.quote
}

type Attributed struct {
    who   string
    quote string
}

func (p Attributed) Say() string {
    return p.who + ": " + p.quote
}


func Replay(conversation []Statement) {
    for _, s := range conversation {
        fmt.Println(s.Say())
    }
}

func (q Quotes) toStatements() Statements {
    conv := make(Statements, len(q))
    for i, v := range q {
        conv[i] = Statement(v)
    }
    return conv
}

func main() {
    conversation := Statements{
        Quote{"Nice Guy Eddie: C'mon, throw in a buck!"},
        Quote{"Mr. Pink: Uh-uh, I don't tip."},
        Attributed{"Nice Guy Eddie", "You don't tip?"},  // <= another type
        Quote{"Mr. Pink: Nah, I don't believe in it."},
        Quote{"Nice Guy Eddie: You don't believe in tipping?"},
    }

    myquotes := Quotes{
        Quote{"Nice Guy Eddie: C'mon, throw in a buck!"},
        Quote{"Mr. Pink: Uh-uh, I don't tip."},
        Quote{"Nice Guy Eddie: You don't tip?"},
        Quote{"Mr. Pink: Nah, I don't believe in it."},
        Quote{"Nice Guy Eddie: You don't believe in tipping?"},
    }

    Replay(conversation)
    Replay(myquotes.toStatements())
}

Replay() doesn't change or know anything about Attributed{}. You do have to introduce types for the slices Quotes & Statements.

like image 32
AndrewN Avatar answered Sep 28 '22 02:09

AndrewN


The in-memory layout of a []Quote slice is different to a []Statement slice, so this is not possible.

The backing array of the []Quote slice will consist of the sequential Quote structs, while the []Statement slice's backing array consists of interface variables. As well as holding the Quote struct (or whatever other type implements the interface), the interface variable also stores a pointer to type information for the contained value. This is needed to determine how to dispatch the Say method call.

The different data layout means that you can't interchange the two slice types, not even through unsafe casts: if you have one type and need the other you'll need to manually convert between them.

like image 57
James Henstridge Avatar answered Sep 28 '22 02:09

James Henstridge


The very short answer to your (long) question is: No.

I don't think that your solution of ConvertToStatment and Replay taking an empty interface is a "nice" solution: I'd prefer func Replay([]Statement) and callers must provide a slice of Statments. This is much clearer and callers can either convert their stuff to []Statement or directly construct a []Statement.

like image 31
Volker Avatar answered Sep 28 '22 01:09

Volker