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?
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 woofRunners
s 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 append
ing 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
woofer
s, a []woofRunner
is just as good as a []woofer
),woofRunner
s, a []woofer
is just as good as a []woofRunner
),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.