35

I'm currently working through the Tour of Go, and I thought that goroutines have been used similarly to Python generators, particularly with Question 66. I thought 66 looked complex, so I rewrote it to this:

package main

import "fmt"

func fibonacci(c chan int) {
    x, y := 1, 1

    for {
        c <- x
        x, y = y, x + y
    }
}

func main() {
    c := make(chan int)
    go fibonacci(c)

    for i := 0; i < 10; i++ {
        fmt.Println(<-c)
    }
}

This seems to work. A couple of questions:

  1. If I turn up the buffer size on the channel to say, 10, fibonacci would fill up 10 further spots, as quickly as possible, and main would eat up the spots as quickly as it could go. Is this right? This would be more performant than a buffer size of 1 at the expense of memory, correct?
  2. As the channel doesn't get closed by the fibonacci sender, what happens memory-wise when we go out of scope here? My expectation is that once c and go fibonacci is out of scope, the channel and everything on it gets garbage-collected. My gut tells me this is probably not what happens.
mazznoer
  • 103
  • 2
  • 9
cflewis
  • 7,251
  • 7
  • 28
  • 37
  • The answer from Hjulle: http://stackoverflow.com/a/16839039/1400793 seems a lot better than any other solution. I wonder why it's not got more upvotes? – Paul Hankin Apr 03 '15 at 02:55

4 Answers4

26

Yes, increasing the buffer size might drastically increase the execution speed of your program, because it will reduce the number of context switches. Goroutines aren't garbage-collected, but channels are. In your example, the fibonacci goroutine will run forever (waiting for another goroutine to read from the channel c), and the channel c will never be destroyed, because the fib-goroutine is still using it.

Here is another, sightly different program, which does not leak memory and is imho more similar to Python's generators:

package main

import "fmt"

func fib(n int) chan int {
    c := make(chan int)
    go func() {
        x, y := 0, 1
        for i := 0; i <= n; i++ {
            c <- x
            x, y = y, x+y
        }
        close(c)
    }()
    return c
}

func main() {
    for i := range fib(10) {
        fmt.Println(i)
    }
}

Alternatively, if you do not know how many Fibonacci numbers you want to generate, you have to use another quit channel so that you can send the generator goroutine a signal when it should stop. This is whats explained in golang's tutorial https://tour.golang.org/concurrency/4.

tux21b
  • 90,183
  • 16
  • 117
  • 101
  • Yes, my main goal of the rewrite was for the caller to not have to specify how many iterations it wants to run through (there are good use cases for not knowing the answer, eg. finding out how many fib numbers are less than 500, and breaking when you get there). Having the quit channel would work, but I did find #66 complex and I thought maybe it was overly complex for the sake of argument. – cflewis Jul 08 '12 at 21:43
  • Your code has another problem. If you break from your for loop. You will be stuck in the same situation as the OP code. – Kugel Nov 16 '12 at 12:00
  • Do you, by any chance, know the difference between setting `chan int` or `<-chan int` as the return type? Like in the [the third code-block](https://blog.carlmjohnson.net/post/on-using-go-channels-like-python-generators/) here? – kendfss Nov 02 '21 at 22:30
  • 1
    @kendfss `chan int` is a channel that can be used for sending and receiving. `<-chan int` can only be used for receiving and `chan<- int` can only used for sending. In this case a channel that can only be used for receiving is enough. – tux21b Nov 04 '21 at 23:30
17

I like @tux21b's answer; having the channel created in the fib() function makes the calling code nice and clean. To elaborate a bit, you only need a separate 'quit' channel if there's no way to tell the function when to stop when you call it. If you only ever care about "numbers up to X", you can do this:

package main

import "fmt"

func fib(n int) chan int {
    c := make(chan int)

    go func() {
        x, y := 0, 1

        for x < n {
            c <- x
            x, y = y, x+y
        }

        close(c)
    }()

    return c
}

func main() {
    // Print the Fibonacci numbers less than 500
    for i := range fib(500) {
        fmt.Println(i)
    }
}

If you want the ability to do either, this is a little sloppy, but I personally like it better than testing the condition in the caller and then signalling a quit through a separate channel:

func fib(wanted func (int, int) bool) chan int {
    c := make(chan int)

    go func() {
        x, y := 0, 1

        for i := 0; wanted(i, x); i++{
            c <- x
            x, y = y, x+y
        }

        close(c)
    }()

    return c
}

func main() {
    // Print the first 10 Fibonacci numbers
    for n := range fib(func(i, x int) bool { return i < 10 }) {
        fmt.Println(n)
    }

    // Print the Fibonacci numbers less than 500
    for n := range fib(func(i, x int) bool { return x < 500 }) {
        fmt.Println(n)
    }
}

I think it just depends on the particulars of a given situation whether you:

  1. Tell the generator when to stop when you create it by
    1. Passing an explicit number of values to generate
    2. Passing a goal value
    3. Passing a function that determines whether to keep going
  2. Give the generator a 'quit' channel, test the values yourself, and tell it to quit when appropriate.

To wrap up and actually answer your questions:

  1. Increasing the channel size would help performance due to fewer context switches. In this trivial example, neither performance nor memory consumption are going to be an issue, but in other situations, buffering the channel is often a very good idea. The memory used by make (chan int, 100) hardly seems significant in most cases, but it could easily make a big performance difference.

  2. You have an infinite loop in your fibonacci function, so the goroutine running it will run (block on c <- x, in this case) forever. The fact that (once c goes out of scope in the caller) you won't ever again read from the channel you share with it doesn't change that. And as @tux21b pointed out, the channel will never be garbage collected since it's still in use. This has nothing to do with closing the channel (the purpose of which is to let the receiving end of the channel know that no more values will be coming) and everything to do with not returning from your function.

Darshan Rivka Whittle
  • 32,989
  • 7
  • 91
  • 109
  • Great answer, thanks Darshan! I'm going to leave the answer with tux as I feel weird about passing answer credit around, but passing the function to the goroutine to determine when to stop was a bit of genius that hadn't crossed my mind. Nice! – cflewis Jul 11 '12 at 05:43
  • 1
    @Lewisham Thanks, I'm glad you liked it! My strongest language is Ruby, where it's common to pass "blocks" in situations like this. First-class functions in Go are wonderful. Regarding your "weird feeling": I don't think tux21b or I are personally invested in having that green check mark. It's by design that you can change it, and the expectation is that you select the post that best answers your question, even if that sometimes means "taking back" a few points from someone. See, for example: http://meta.stackexchange.com/a/28279/187298 In short, don't worry about hurting our feelings. :) – Darshan Rivka Whittle Jul 11 '12 at 06:38
13

You could use closures to simulate a generator. Here is the example from golang.org.

package main

import "fmt"

// fib returns a function that returns
// successive Fibonacci numbers.
func fib() func() int {
    a, b := 0, 1
    return func() int {
        a, b = b, a+b
        return a
    }
}

func main() {
    f := fib()
    // Function calls are evaluated left-to-right.
    fmt.Println(f(), f(), f(), f(), f())
}
Hjulle
  • 2,471
  • 1
  • 22
  • 34
  • 1
    Note that this won't work for generators that don't consist of a simple infinite `for`. A more realistic generator might contain nested loops, `break` and `continue` statements, `yield` inside `if`, and so on. None of those can be easily translated into a closure. – user4815162342 Dec 09 '18 at 11:38
  • 1
    @user4815162342 In principle, all such constructs can be converted into a state machine. But you're right that it won't always be easy or pretty if you do it manually. – Hjulle Dec 09 '18 at 14:15
  • Exactly, that's why I wrote "easily". Manually constructed state machines fashioned out of closures are what's usually known as _callback hell_. – user4815162342 Dec 09 '18 at 17:11
8

Using channels to emulate Python generators kind of works, but they introduce concurrency where none is needed, and it adds more complication than's probably needed. Here, just keeping the state explicitly is easier to understand, shorter, and almost certainly more efficient. It makes all your questions about buffer sizes and garbage collection moot.

type fibState struct {
    x, y int
}

func (f *fibState) Pop() int {
    result := f.x
    f.x, f.y = f.y, f.x + f.y
    return result
}

func main() {
    fs := &fibState{1, 1}
    for i := 0; i < 10; i++ {
        fmt.Println(fs.Pop())
    }
}
Paul Hankin
  • 54,811
  • 11
  • 92
  • 118
  • 2
    You could use closures to make hide the state and make the code more similar to the code in the question. – Hjulle May 30 '13 at 14:16
  • The channel solution is obviously better when dealing with problems that are inherently recursive. Imagine generating all permutations of (an array / a slice). You can either: (1) Write a recursive function, pass the channel around, and send each permutation found in the base case of the recursion to the channel. The problem with this solution is that if the caller doesn't need all permutations, and decided to stop in the middle, it needs to close the channel manually, or there will be resource leakage. – Siu Ching Pong -Asuka Kenji- Dec 24 '15 at 13:01
  • (2) Write a recursive function, pass the "consumer function" around, and consume each permutation in the base case of the recursion. There are 2 problems with this solution. One is that the "library writer" has to make an assumption on the signature of the "consumer function". `func (T)`, where `T` is the type of the result, is generally acceptable. Another is that the flow is controlled by the "library function". Even if a "termination condition function", like `wanted()` in Darshan-Josiah Barber's answer above, is added, it is no more pure and generic than the channel solution. – Siu Ching Pong -Asuka Kenji- Dec 24 '15 at 13:17
  • (3) Write a recursive function, use a closure to encapsulate all the states required to start the next iteration. But this is essentially saving all variables in the whole stack of the recursion, so it is not much less work than transforming the algorithm to iterative style. The problem with this solution is that, as mentioned in my previous comment, not all recursive algorithms could easily (and/or elegantly) be transformed to iterative ones. – Siu Ching Pong -Asuka Kenji- Dec 24 '15 at 13:30