Prime Numbers as Streams, With RxJS and Go
By: Enrico Piccinin
Tags: Reactive Programming, Rxjs, Programming, Go, Golang

“Sieve of Eratosthenes” Using Go Channels as Streams

In Go we can use channels as a mean to implement streams. For instance an unbound stream of integers starting from 2 can be generated like this:

ch := make(chan int)

for i := 2; ; i++ {

ch <- i

}

In this case the stream is represented by the channel ch . Any client code that wants to read the stream has just to read the channel ch .

Our logic asks to create recursively streams of non-multiples of a prime number starting from an input stream. We can code a filter function to implement such logic using the stream-as-channel concept:

func filter(inStream <-chan int, filteredStream chan<- int, n int) {

for i := range inStream {

if i%n != 0 {

filteredStream <- i

}

}

}

inStream is the channel representing the input stream and filteredStream is a channel representing the stream of the non multiples of n , where n is the prime number. Note the use of <-chan and chan<- . The type of inStream is <-chan int , which means a channel from which we can only read, while fitleredStream is of type chan<- int , which means it is a channel where we can only write.

Finally we need the pNumbers function which, given an input stream, takes the first value, which is a prime number, applies the filtering logic to the rest of the stream to create a new “filtered” stream and calls itself recursively with this new “filtered” stream as input stream:

func pNumbers(inStream <-chan int, lastPrime int) {

primeNumber := <-inStream

fmt.Println(i, primeNumber)

filteredStream := make(chan int)

go filter(inStream, filteredStream, primeNumber)

pNumbers(filteredStream, primeNumber)

}

The final version of the full solution is just a composition of the building blocks we’ve just created.

Calculates the first 100 prime numbers

It’s worth noticing that all the recursive calls to the function pNumbers run in the same “main” goroutine, while each of the calls to generate and filter run in their own goroutines. The basic reason is that pNumbers is a just consumer of the streams, in other words it just reads from the channels created, and it has to work sequentially since we want an ordered sequence of prime numbers. A consumer needs producers, i.e. functions that write on the channels. generate is a producer, since it only writes on a single channel. filter is a consumer and a producer, since it reads from one channel and writes on another channel. Consumers and producers of the same stream, i.e. readers and writers on the same channel, need to run in separate goroutines to avoid deadlocks.

The various interactions among goroutines and channels for the first three prime numbers after 1 (i.e. 2, 3, 5) is illustrated below.

goroutines and channels interactions while calculating the first 3 prime numbers after 1

The sequence of the actuals reads and writes may be slightly different. For instance, the goroutine gr2 can write 3 to source chan before gr1 creates chan 2. However, from a logical standpoint the above diagram is a clear representation of the interactions happening during the process.

It’s also interesting to look at the way the calculation is closed. The shutdown starts with the generate function exiting its loop when the upTo value has been reached and then closing the channel source chan, i.e. the channel it writes to. source chan is the channel from which the filter function running in gorourine gr3 reads. So, when this function tries the next read from source chan , it finds a closed channel and therefore exits the for range loop and closes the channel chan 2 , i.e. the channel where it writes to. chan 2 is the channel which the filter function running in gr4 reads from. But now chan 2 is closed, which leads to closing chan 3. And so on and so forth, until we reach the last channel created by pNumbers , i.e. the channel gr1 wants to read from. But this read operation can’t complete, since the channel is closed before anything is written into it, so pNumbers exits, causing the whole program to exit.

This sequence is illustrated here:

The sequence of closing channels and finishing the program

The above implementation is a variation, focused on the stream concept, of the “Prime Sieve” in Go, a very elegant solution of the same problem you can find explained in this interesting talk about the why’s of Go.

The Go Prime Sieve implementation

The beauty of concurrency is that you can leverage all of your cores

The Go implementation of the “Sieve of Eratosthenes” heavily leverages concurrency. To find the first X prime numbers (starting from two), we create X goroutines which run concurrently. We have organised the logic in a concurrent way, so that we can deal with many things at the same time. These many things are the various filtering operations that are happening in a concurrent way.

The execution is not necessarily parallel, in the sense that it also works with just one available core. But if there are many cores parallelism can be derived very naturally. The Go runtime will make sure that all cores are used, reducing the execution time the number of cores increases.