Generators in GO

Generator, simply, this is a function that returns the iterator. It's applicable in case of when you do not need to load all data in memory before processing or in cases when we can not receive all data at once or we generate it on the go. In some languages mainly used special keyword yield what returns some value until the next call of the next value from the iterator.

Let’s create the generator.

def gen(steps):
	for i in range(0, steps):
		yield i * i


The yield will generate something like this

class genIter():
    def __init__(self, max = 0):
        self.max = max

    def __iter__(self):
        self.n = 0
        return self

	def __next__():
	    if self.n > self.max:
	        raise StopIteration
		res = self.n * self.n
		self.n += 1
		return res


In GO
But in the «go» no special syntax to declaring the generators, and we can’t implement some special interface, but it’s possible to use some specific of chan in *for range*, specifically it’s iterable, plus we need to use goroutine.

Let’s make the generator in the go

func genSquare(count int) <-chan int {
	ch := make(chan int, 3)
	go func() {
		defer close(ch)
		for i := 1; i < count; i++ {
			ch <- i * i
		}
	}()
	return ch
}


Execute the generator

func main() {
	for i := range genSquare(11) {
		fmt.Println("square", i)
	}
}


Run the main application

square 1
square 4
square 9
square 16
square 25
square 36
square 49
square 64
square 81
square 100


It works fine but here we have one problem: We can’t interrupt the loop in the goroutine.

It could be fixed, but it’s not really convenient. Now we can little bit change our example:

func genSquare(count int, quit ...<-chan struct{}) <-chan int {
	ch := make(chan int, 3) // the problem with buffer, read below. Must be ch := make(chan int)
	go func() {
		defer close(ch)
		for i := 1; i < count; i++ {
			if len(quit) > 0 && quit[0] != nil {
				select {
				case <-quit[0]:
					return
				case ch <- i * i:
				}
			} else {
				ch <- i * i
			}
		}
	}()
	return ch
}


Now function as a second param accept the chan and immediately we get the problem because we have an internal buffer.

func main() {
	quit := make(chan struct{}) // ! The probleme is here
	defer close(quit)
	for i := range genSquare(11, quit) {
		fmt.Println("square", i)
		if i > 40 {
			quit <- struct{}{}
		}
	}
}


The chan with the undeclared size of the buffer for every read and writes operation blocks the thread until someone will write or read from it. In our case *genSquare* finished the generate data before received the quit signal and the thread will blocks. It could be fixed if we will remove the buffer from generator function *ch := make(chan int)*.

Conclusion

The go version of generators is not as simple as in the python or other languages with support of yield syntax. The amount of code in go much more then in python, 3 lines in python vs 18 lines in go. It related with that in go no special syntax for creating of generators and the developers forced to invent other ways of implementation of it. Moreover, the approach of the go is more transparent for developers then the magical constructions which hiding the details of implementation. And you have more control on the behavior which you could design yourself.

Meanwhile waiting for the generics in the «go» version 2 ;)

0 comments

Only registered users can comment.