Go Interface Implementation with Wider Method Signature

go

In Go, is there a way to implement an interface using a method, where the return type of the corresponding method in the implementation is a "wider than" the expected return type?

This is difficult to explain so here is an example. I get this error when running the below example code in Go Playground:

./prog.go:36:14: cannot use BirdImpl{} (type BirdImpl) as type Animal in argument to foo:
    BirdImpl does not implement Animal (wrong type for Move method)
        have Move() BirdMoveResult
        want Move() AnimalMoveResult

(where BirdMoveResult is "wider than" AnimalMoveResult because any implementation of BirdMoveResult is also an implementation of AnimalMoveResult)

package main

type (
    Animal interface {
        Move() AnimalMoveResult
    }
    Bird interface {
        Move() BirdMoveResult
    }
    AnimalMoveResult interface {
        GetDistance() int
    }
    BirdMoveResult interface {
        GetDistance() int
        GetHeight() int
    }
    BirdImpl struct{}
    BirdMoveResultImpl struct{}
)

// Some implementation of BirdImpl.Move
// Some implementation of BirdMoveResultImpl.GetDistance
// Some implementation of BirdMoveResultImpl.GetHeight

func foo(animal Animal) int {
    return animal.Move().GetDistance()
}

func main() {
    foo(BirdImpl{})  // This fails because BirdImpl doesn't implement Animal. My question is why not?
}

I understand that the Move() method signatures do not match completely because of the different return types, thus Go does not consider BirdImpl as an implementation of Animal. However, if Go compares the return types, any implementation of BirdMoveResult would also implement AnimalMoveResult. So, shouldn't Move() BirdMoveResult be an acceptable implementation for Move() AnimalMoveResult (and if not, why not)?

Edit: In the actual scenario, Animal, AnimalMoveResult, and foo are part of an external package. Within my own code, I want to be able to extend AnimalMoveResult with my own interface methods (as it's done in the example with BirdMoveResult), while still being able to make use of foo with using the extended interface.

Best Answer

These kinds of problems are usually the result of:

  • Premature interfaces
  • Fake interfaces, or "bubble-wrapping" (see my rant here)
  • Parallel interfaces

Your error you have is most specifically related to the 3rd issue, but each is present in your example design here.

Good interfaces should convey (in their name and their method signatures) a set of behaviour. The name of the interface should be implied by method(s) it contains, as the name is simply supposed to capture the essence of that behaviour.

The only interface seemingly needed for this purpose is:

type Mover interface {
    Move() MoveResult
}

For MoveResult, you can do this (exchange float with int if you like):

type MoveResult struct {
    Distance, Height float64
}

This type can potentially function just fine as a struct, given the following assumptions:

  • Move results are just data points (they don't need to be dynamic; you can set the values and leave them)
  • "Extra" data like Height function properly with a zero value when it's not specified.

Now, implement the bird type:

type Bird struct {
    // some internal data
}

func (b *Bird) Move() MoveResult {
    // some movement calculations
    return MoveResult{Distance: howFar, Height: howHigh}
}

Now implement foo:

func foo(m Mover) float64 {
    return m.Move().Distance
}

Now use it in main:

func main() {
    foo(&Bird{})
}

Now we can add other kinds of Movers to the program and use them with foo without issue.


Addressing your comment, where Animal, AnimalMoveResult and foo are all external and you can't modify them:

How I'm posing the problem with my understanding: There is this function foo, designed to accept an interface called Animal, which is characterized by how it returns an proprietary type. You want to use your type Bird as an Animal, but you can't fit all of the behaviour you want into that proprietary return type.

So how to resolve this?

Implement your own, wider return type

type birdMoveResult struct {
    // some internal data
}

func (r birdMoveResult) GetDistance() int {
    // get the distance
}

func (r birdMoveResult) GetHeight() int {
    // get the height
}

Now, implement the Bird type

type Bird struct {
    // some internal data
}

func (b *Bird) Move() AnimalMoveResult {
    var result birdMoveResult
    // calculate the move result
    return birdMoveResult
}

Notice that Move returns the AnimalMoveResult type, so now Bird will satisfy the Animal interface. We are able to do this because birdMoveResult satisfies the AnimalMoveresult type, so returning it here is legal.

The remaining issue is that Bird's Move method narrows the wide interface, we've effectively lost that new functionality you added. In order to get it back while still satisfying the Animal interface, we can't change the Move method signature at all. Here are 2 possible workarounds.

  1. When using your bird type, you can type-assert the result to gain access to the extra method
var b Bird
// ...
height := b.Move().(birdMoveResult).GetHeight()
  1. Add a new method to Bird that returns the wider type, leaving the existing Move with the same signature
func (b *Bird) MoveFull() birdMoveResult {
    var result birdMoveResult
    // calculate the move result
    return birdMoveResult
}

func (b *Bird) Move() AnimalMoveResult {
    return b.MoveFull()
}

height := b.MoveFull().GetHeight()

Either of these will work, it's mostly a matter of preference.

Related Question