DEV Community

Cover image for Go channels in JS (Bonus 1): nil channel
Nicolas Lepage for Zenika

Posted on

Go channels in JS (Bonus 1): nil channel

This is a bonus post in the "Go channels in JS" series about how I wrote in JavaScript the equivalent of Go(lang) channels.
If you haven't already, I recommend reading at least the first post before reading this one:

So did you know that Go allows using nil channels?
Now let's see how and why this is possible.

nil channel

Let's start by clarifying what is a nil channel.

So far when we wanted to create a channel, we used the make builtin function:

ch := make(chan int)

make returns a pointer to a channel, so a nil channel is just a nil pointer, in other words no channel at all:

// This a nil integer channel pointer:
var ch chan int

// Declared in a more explicit way:
var ch chan int = nil

So why would we need a nil channel?

You would think that sending to or receiving from a nil channel is an illegal operation, but it is actually allowed.
Both will block indefinitely!

Now the next question is how is this useful?
We don't want a goroutine to be blocked forever, this is actually a well known problem: a goroutine leak.

Well there is the select statement we haven't spoken of so far, which allows to wait for several channel operations at the same time:

func PrintValues(ints chan int, strings chan string) {
    for {
        select {
        case i := <-ints:
            fmt.Printf("Received integer: %d\n", i)
        case s := <-strings:
            fmt.Printf("Received string: %s\n", s)
        }
    }
}

But what if the sender closes the ints channel?
Receiving from a closed channel returns a nil value, so PrintValues will print "Received integer: 0" on the standard output indefinitely!

In order to avoid that, it is possible to use a nil channel to disable one case of the select:

func PrintValues(ints chan int, strings chan string) {
    for {
        select {
        case i, ok := <-ints:
            if !ok {
                ints = nil
                break
            }
            fmt.Printf("Received integer: %d\n", i)
        case s := <-strings:
            fmt.Printf("Received string: %s\n", s)
        }
    }
}

As soon as the ints channel is closed, we replace it by a nil pointer, which disables the first case of the select.

Of course we have to do the same for the strings channel, but it would end up blocking the entire select, and the goroutine executing it...
PrintValues must return when both channels are closed:

func PrintValues(ints chan int, strings chan string) {
    for {
        select {
        case i, ok := <-ints:
            if !ok {
                if strings == nil {
                    return
                }
                ints = nil
                break
            }
            fmt.Printf("Received integer: %d\n", i)
        case s, ok := <-strings:
            if !ok {
                if ints == nil {
                    return
                }
                strings = nil
                break
            }
            fmt.Printf("Received string: %s\n", s)
        }
    }
}

Run it on Go playground

Now that we know what nil channels may be used for, let's add the same feature to our JS channels.

Implementing nil channels

As our JS channels don't have a select for now, our implementation of nil channels will be partial.

The equivalent of a nil channel in JS will be a null or undefined channel.

So far when we created or executed send and receive operations, we didn't check at all that the channel key was actually defined or different of null.

Hence sending to or receiving from a null/undefined channel would have ended up in a TypeError somewhere in our code.

Now let's modify the existing send operation in order to accept null/undefined channel keys and return a never resolved Promise:

export const channelMiddleware = () => (next, ctx) => async operation => {
  // ...

  if (operation[SEND]) {
    if (!operation.chanKey) return new Promise(() => {})

    // Actually perform send operation...
  }

  // ...
}

The receive operation uses the doRecv() function, which is mutualized with the range operation (see previous post).
So let's modify the doRecv() function to also accept null/undefined channel keys and return a never resolved Promise:

const doRecv = async (ctx, chanKey) => {
  if (!chanKey) return new Promise(() => {})

  // Actually perform receive operation...
}

And that's it!
Of course, we just implemented the "bad part" of nil channels, and we will have to add the good part next time when implementing the select...

What next

Next time we will finally implement the select, and complete the full feature set of channels.

I hope you enjoyed this small bonus post, give a ❀️, πŸ’¬ leave a comment, or share it with others, and follow me to get notified of my next posts.

Top comments (0)