Question

I like to program providing interfaces as restrictively as possible, both to avoid bad usage as to be explicit and self-documented.

So, I like to provide directional channels when they are supposed to be used unidirectionally by the user, but of course, internally I have a bidirectional channel copy.

Assigment for the following works:

var internal chan int
var external <-chan int
external = internal

But now I want to provide to the user a <-chan chan<- int type (in the return of a function), but the following won't work:

var internal chan chan int
var external <-chan chan<- int
external = internal // this fails

I have two questions:

  • Exactly why that doesn't work?
  • So, I can declare a variable of <-chan chan<- type, but... can't use such a type in any practical sense? (Because even though there're directional channels, they're AFAIK aways used in orchestration with bidirectional ones, and since assignment is not possible, they can't be used this way)
Was it helpful?

Solution

The reason why this does not work

The specification says this about channel assignability:

A [channel] value x is assignable to a variable of type T ("x is assignable to T") [when] x is a bidirectional channel value, T is a channel type, x's type V and T have identical element types, and at least one of V or T is not a named type.

This reflects exactly what you're experiencing:

  • chan (chan int) to <- chan (chan int) works
  • chan (chan int) to <- chan (chan<- int) does not

The reason for this is that the element types (the ones after the chan keyword) are not equal.

I can declare it but not use it?

You can use it but not the way you want to. It is not possible to assign the variables the way you do but by correcting the element types you can indeed use it:

var internal chan chan<- int
var external <-chan chan<- int
external = internal

If you only have your chan chan int type you need to copy your values (Examples on play):

var internal chan chan int
var internalCopy chan chan<- int

go func() { for e := range internal { internalCopy <- e } }()

var external <-chan chan<- int
external = internalCopy

OTHER TIPS

This is a case somewhat akin to the problems of covariance and contravariance encountered in languages with user-level generics. You can also encounter it in Go when using its internal equivalents of generic types (they're called 'composite types'). For instance:

type A struct{}

// All `B`s are convertible to `A`s
type B A

Or even:

type A interface{}
// B embeds A
type B interface{ A }

var b B
// This works without problem
var a A = A(b)

But consider the following case:

var bs []B
var as []A = ([]A)(bs)

Here, the compilation fails with error cannot use bs (type []B) as type []A in assignment. Although any B can be converted to the equivalent A, this does not hold true for []B and []A (or chan B and chan A, or map[string]B and map[string]A, or func(a A) and func(b B) or any other generic type using As and Bs in their definition). Although types are convertible to one another, they are not the same, and the way these generics work in Go is, from the spec:

Each type T has an underlying type: If T is a predeclared type or a type literal, the corresponding underlying type is T itself. Otherwise, T's underlying type is the underlying type of the type to which T refers in its type declaration.

type T1 string
type T2 T1
type T3 []T1
type T4 T3

The underlying type of string, T1, and T2 is string. The underlying type of []T1, T3, and T4 is []T1.

Note here that the underlying type of []T1 is []T1, not []string. This means that the underlying type of []T2 will be []T2, not []string or []T1, which makes conversion between them impossible.

Basically, you are trying to do something like:

var internal chan Type1
var external <-chan Type2
external = internal

Which fails as Type1 and Type2 are two different types, as far as the type system is concerned.

Covariance and contravariance are very difficult problems, as the length of the wikipedia article or any time spent untangling layers of bounded generics in Java or C# will tell you. It is one of the reason generics are so difficult to implement and raise so many debate.

You can get the behaviour you want by going one level deeper on your aliases between read-only and read/write channels, exactly as you did with your internal/external channel on your first example:

package main

import "fmt"

// This has the correct signature you wanted
func ExportedFunction(c <-chan (chan<- int)) {
    // Sends 1 to the channel it receives
    (<-c)<- 1
}

func main() {
    // Note that this is a READ/WRITE channel of WRITE-ONLY channels
    // so that the types are correct
    internal := make(chan (chan<- int))
    var external <-chan (chan<- int)
    // This works because the type of elements in the channel is the same
    external = internal

    // This channel is internal, so it is READ/WRITE
    internal2 := make(chan int)

    // This is typically called externally
    go ExportedFunction(external)

    fmt.Println("Sending channel...")
    // The type of internal makes the receiving end of internal/external
    // see a WRITE-ONLY channel
    internal <- internal2
    fmt.Println("We received:")
    fmt.Println(<-internal2)
}

The same thing on the playground.

Basically the same thing as your first example, except that you have to go one level deeper in the 'read/write' vs 'read(or write) only' aliases.

I doubt that this is possible as you would have to convert the type of the outer and inner channel at the same time. One at a time works.

I like to think of these send/receive-only channels a a nice way to limit stuff you can do in a function: You have a var c chan int and you pass it to a func f(ro <-chan int) and now in f you are save from sending to ro. No explicit type conversion needed. Same for returning c: You may just return c in func g() <-chan int. But on any case you must agree what type to transfer via your your channel: This can be an other bidirectional channel, a send-only or a receive-only channel. Once you got that sent channel out you may convert the channel.

chan chan int is a channel of integer channels. You can convert this to a receive-only channel of integer channels <-chan chan int or a send-only channel of integer channels chan<- chan int. Anyway: What you send or receive is always a integer channel chan int. Each such can be converted to send/receive-only before/after sending/receiving but you cannot convert chan chan int to chan chan<-int as this is like converting chan chan int to chan chan int32 or even chan string.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top