This overlap also struck me as something worth considering.
Just to add some more data points to your disjunction section:
When talking about composite types using interfaces or type variables, the difference becomes more apparent:
func Concat(type T stringer)(ts ...T) stringer
vs
func Concat(ts ...fmt.Stringer) string
On the type-parameterized case once you pick the concrete type, all the elements of the slice must be of the same type.
On the other hand this means that you can use existing slices.
One of the most common questions I found when teaching Go to newcomers is "why can't I convert a slice of X where X implements Y into a slice of Y"?
i.e.:
type sint int
func (s sint) String() string { return fmt.Sprint(int(s)) }
func demo(vs []fmt.Stringer) {
for _, v := range vs {
fmt.Println(v.String())
}
}
func main() {
a := []sint{1, 2, 3}
s := make([]fmt.Stringer, len(a))
for i := range a {
s[i] = a[i]
}
demo(s)
}
with contracts you can just pass a concrete instance of the composite type:
func demo(type T fmt.Stringer)(vs []T) {
for _, v := range vs {
fmt.Println(v.String())
}
}
func main() {
a := []sint{1, 2, 3}
demo(a)
}
So, even putting aside runtime optimizations, there are perfectly valid use cases where you either want e.g. "a slice of any value as long as each value implements a given method" or "a slice of a concrete type which implements a given method".
The former allows you to lift your value into the abstraction (e.g. you can pass your implementation of an io.Writer anywhere some code expects one). This is generally unidirectional; e.g. the external code doesn't give you back an instance of your own type (because in order to do it, you'd have to employ a runtime type assertion to get it back).
The latter allows you to consume some abstraction on top of your data. This code can produce new values of your types (without hacks like factory interfaces) and the compiler knows about that.