How to return a named channel type that is read-only version of another channel type?

56 Views Asked by At

I have a code that is similar to this:

type Ch chan string

func getCh() Ch {
    ch := make(Ch)
    go func() {
        defer close(ch)
        ch <- "foo"
    }()
    return ch
}

func main() {
    fmt.Println(<-getCh())
}

What I would like to do is to return a read-only version of the Ch channel from the getCh function. I've adding a read-only type that is compatible with the Ch channel, and changing the function signature to use this type like this:

type Ch chan string
type ChRo <-chan string

func getCh() ChRo {
    ch := make(Ch)
    // ...
    return ch
}

but this gives a compile error:

cannot use ch (variable of type Ch) as ChRo value in return statement

However if I drop the ChRo type and use native <-chan string as return type it works:

func getCh() <-chan string {
    ch := make(Ch)
    // ...
    return ch
}

Is there a way to use the ChRo type for the return value? The channel in my real code is not actually of string type - it is quite more complex generic struct.

As I understand it - the ChRo type matches exactly the type <-chan string. There is no covariance confusion going on here.

Why

  1. The first goal here is to have return a read-only channel type. It should be a separate type for readability. And it should be read-only so the consumer won't use it in a write context.
  2. The second goal is to have the getCh function compatibile with another function that will be internally called by the getCh:
func getCh() <-chan string {
    data, err := fetchFromExternalResource() // returns (ChRo, error)
    // handle error
    return data
}
1

There are 1 best solutions below

0
Markus W Mahlberg On

Think of it like this: The type cannot (and imho) should not decide on how it is used. Interfaces define behavior, functions implement it.

So, the way to go would be to define one or more interfaces which define the desired behavior.

Having that said, what you want can be achieved. Given the following code:

package main

import "fmt"

type MyChannel <-chan string

type Stuff interface {
    // DoSomething implicitly requires a read-only channel.
    DoSomething(MyChannel)
}

type Reader struct {
    Prefix string
}

func (ms *Reader) DoSomething(ch MyChannel) {
    fmt.Printf("%s, %s!\n", ms.Prefix, <-ch)
}

type Writer struct {
    Prefix string
}

func (ms *Writer) DoSomething(ch MyChannel) {
    ch <- fmt.Sprintf("%s, %s", ms.Prefix, "world")
}

func callStuff(stuff Stuff, ch MyChannel) {
    stuff.DoSomething(ch)
}

func main() {
    s := Reader{"Hello"}
    ch := make(chan string)
    go func() {
        ch <- "world"
    }()
    callStuff(&s, ch)

    w := Writer{"Goodbye"}
    callStuff(&w, ch)
    fmt.Printf("Received: %s\n", <-ch)
}

This won't even compile, since the compiler correctly deduces that in func (ms *Writer) DoSomething(ch MyChannel), we are trying to write to a receive-only channel:

./prog.go:25:2: invalid operation: cannot send to receive-only channel ch (variable of type MyChannel)

Without the Writer, it works as expected. Run on playground