Search code examples
gocovariance

Why can't I substitute a slice of one type for another in Go?


I'm trying to understand Go's type conversion rules. Say we have these interfaces:

type woofer interface {
  woof()
}

type runner interface {
  run()
}

type woofRunner interface {
  woofer
  runner
}

and to satisfy the interfaces we have a dog type:

type dog struct{}

func (*dog) run()  {}
func (*dog) woof() {}

These two functions are using the interfaces:

func allWoof(ws []woofer) {}

func oneWoof(w woofer) {}

To use these methods I can write the following:

dogs := make([]woofRunner, 10)
oneWoof(dogs[0])
allWoof(dogs)

The first function oneWoof() works as expected; a *dog implements all oneWoof needs, which is a woof function.

However for the second function allWoof, Go won't compile the attempted invocation, reporting the following:

cannot use dogs (type []woofRunner) as type []woofer in argument to allWoof

Using a type conversion is also impossible; writing []woofer(dogs) fails as well:

cannot convert dogs (type []woofRunner) to type []woofer

Every member of []woofRunner has all the necessary functions to satisfy a []woofer, so why is this conversion prohibited?

(I'm not sure if this is the same case explained in the Go FAQ and in various questions on Stack Overflow in which people ask about converting type T to interface{}. Every pointer in the slice/array is pointing to a type that is directly convertible to another type. Using these pointers should be possible for the same reason that passing dog[0] to 'oneWoof` is possible.)

Note 1: I know one solution is to loop over and and convert the items one by one. My question here is why that's necessary and whether there is a better solution.

Note 2: Regarding the rules of Assignability:

A value x is assignable to a variable of type T [when] T is an interface type and x implements T.

Can't we say if the type of the slice/array is assignable to another type, then arrays of those types are also assignable?


Solution

  • In addition to Go's refusal to convert slices along these variance relationships addressed in other answers here, it's useful to think through why Go refuses to do so, even when the in-memory representation would be the same between the two types.

    In your example, supplying a slice of woofRunnerss as a parameter of type []woofer is asking for covariant treatment of the slice's element type. When reading from the slice, indeed, since a woofRunner is a woofer, you know that every element present in a []woofRunner will satisfy a reader looking for []woofer.

    However, in Go, a slice is a reference type. When passing a slice as an argument to a function, the slice is copied, but the copy used in the invoked function body continues to refer to the same backing array (absent reallocation necessary before appending beyond its capacity). The mutable view of an array—more generally, inserting an item into a collection—requires contravariant treatment of the element type. That is, when it comes to demanding a function parameter with the intention of inserting into or overwriting an element of type woofRunner, it's acceptable to supply a []woofer.

    The question is whether the function is demanding the slice parameter for

    • reading from it (for reading woofers, a []woofRunner is just as good as a []woofer),
    • writing to it (for writing woofRunners, a []woofer is just as good as a []woofRunner),
    • or both (neither is an acceptable substitute for the other).

    Consider what would happen if Go did accept slice parameters in covariant fashion, and someone came along and changed allWoof as follows:

    // Another type satisfying `woofRunner`:
    type wolf struct{}
    func (*wolf) run()  {}
    func (*wolf) woof() {}
    
    func allWoof(ws []woofer) {
      if len(ws) > 0 {
        ws[0] = &wolf{}
      }
    }
    
    dogs := []*dog{&dog{}, &dog{}}
    allWoof(dogs)  // Doesn't compile, but what if it did?
    

    Even if Go was willing to treat a []*dog as a []woofer, we would wind up with a *wolf in our array of *dog here. Some languages defend against such an accident with run-time type checks on the attempted array insertion or overwrite, but because Go precludes us from even making it this far, it doesn't need these additional checks.