What's the point of one-way channels in Go?

A channel can be made read-only to whoever receives it, while the sender still has a two-way channel to which they can write. For example:

func F() <-chan int {
    // Create a regular, two-way channel.
    c := make(chan int)

    go func() {
        defer close(c)

        // Do stuff
        c <- 123
    }()

    // Returning it, implicitely converts it to read-only,
    // as per the function return value.
    return c
}

Whoever calls F(), receives a channel from which they can only read. This is mostly useful to avoid potential misuse of a channel at compile time. Because read/write-only channels are distinct types, the compiler can use its existing type-checking mechanisms to ensure the caller does not try to write stuff into a channel it has no business writing to.


I think the main motivation for read-only channels is to prevent corruption and panics of the channel. Imagine if you could write to the channel returned by time.After. This could mess up a lot of code.

Also, panics can occur if you:

  • close a channel more than once
  • write to a closed channel

These operations are compile-time errors for read-only channels, but they can cause nasty race conditions when multiple go-routines can write/close a channel.

One way of getting around this is to never close channels and let them be garbage collected. However, close is not just for cleanup, but it actually has use when the channel is ranged over:

func consumeAll(c <-chan bool) {
    for b := range c {
        ...
    }
}

If the channel is never closed, this loop will never end. If multiple go-routines are writing to a channel, then there's a lot of book-keeping that has to go on with deciding which one will close the channel.

Since you cannot close a read-only channel, this makes it easier to write correct code. As @jimt pointed out in his comment, you cannot convert a read-only channel to a writeable channel, so you're guaranteed that only parts of the code with access to the writable version of a channel can close/write to it.

Edit:

As for having multiple readers, this is completely fine, as long as you account for it. This is especially useful when used in a producer/consumer model. For example, say you have a TCP server that just accepts connections and writes them to a queue for worker threads:

func produce(l *net.TCPListener, c chan<- net.Conn) {
    for {
        conn, _ := l.Accept()
        c<-conn
    }
}

func consume(c <-chan net.Conn) {
    for conn := range c {
        // do something with conn
    }
}

func main() {
    c := make(chan net.Conn, 10)
    for i := 0; i < 10; i++ {
        go consume(c)
    }

    addr := net.TCPAddr{net.ParseIP("127.0.0.1"), 3000}
    l, _ := net.ListenTCP("tcp", &addr)
    produce(l, c)
}

Likely your connection handling will take longer than accepting a new connection, so you want to have lots of consumers with a single producer. Multiple producers is more difficult (because you need to coordinate who closes the channel) but you can add some kind of a semaphore-style channel to the channel send.


Go channels are modelled on Hoare's Communicating Sequential Processes, a process algebra for concurrency that is oriented around event flows between communicating actors (small 'a'). As such, channels have a direction because they have a send end and a receive end, i.e. a producer of events and a consumer of events. A similar model is used in Occam and Limbo also.

This is important - it would be hard to reason about deadlock issues if a channel-end could arbitrarily be re-used as both sender and receiver at different times.

Tags:

Go