Simply simulate the generator of Python using the Go language

  • 2020-05-30 20:23:07
  • OfStack


def demo_input_and_output():
  input = yield 'what is the input?'
  yield 'input is: %s' % input

gen = demo_input_and_output()
print(gen.next())
print(gen.send(42))

This code demonstrates the functionality of python generator. You can see that yield is doing two operations at the same time, one is sending data "waht is the input" to the outside, and the other is sending data "input" to the inside. And the operation to receive the data is a blocking operation. If next() is not called externally (that is, None is passed in), or send(42) is called externally (that is, the value 42 is passed in), then the blocking operation will wait.

That is to say, python's generator comes with an channel for external communication, which is used for sending and receiving messages. go simulates python's generator and that's what it looks like

package main
import "fmt"
func demoInputAndOutput(channel chan string) {
    channel <- "what is my input?"
    input := <- channel
    channel <- fmt.Sprintf("input is: %s", input)
} func main() {
    channel := make(chan string)
    go demoInputAndOutput(channel)
    fmt.Println(<- channel)
    channel <- "42"
    fmt.Println(<- channel)
}

This code is basically equivalent to the python version. The implicit channel becomes explicit in the go version. yield becomes channel < -operation, and we did one at once < -block read operation of channel. That's the essence of yield.

channel of go can also be recycled as iterator by for:

package main
import "fmt"
func someGenerator() <-chan string {
    channel := make(chan string)
    go func() {
        channel <- "a"
        fmt.Println("after a")
        channel <- "c"
        fmt.Println("after c")
        channel <- "b"
        fmt.Println("after b")
        close(channel)
    }()
    return channel
} func main() {
    channel := someGenerator()
    for val := range channel {
        fmt.Println(val)
    }
}

It's not yield for python, channel for channel < -is not equivalent to yield, it will go down until it blocks. Effect is

after a
a
c
after c
after b
b

This is not in the expected order. The reason after a after c after b is not printed here is because channel defaults to buffer, which has only one element, so one is blocked. If you increase buffer, you have an effect

make(chan string, 10)

The output becomes:


after a
after c
after b
a
c
b

So goroutine is like a separate thread playing with itself without waiting to be executed. If you want to simulate yield, you need to add the displayed synchronous operation (block the read signal from channel) :

package main
import "fmt"
func someGenerator() chan string {
    channel := make(chan string)
    go func() {
        channel <- "a"
        <- channel
        fmt.Println("after a")
        channel <- "c"
        <- channel
        fmt.Println("after c")
        channel <- "b"
        <- channel
        fmt.Println("after b")
        close(channel)
    }()
    return channel
} func main() {
    channel := someGenerator()
    for val := range channel {
        fmt.Println(val)
        channel <- ""
    }
}

The output is


a
after a
c
after c
b
after b

Here we can see that the generator of python is like the goroutine of golang with an channel without buffer. This results in a coroutine context switch for each yield1 value. Although coroutine context switching is cheap, it is not without cost. A design such as goroutine's buffered channel allows one goroutine to produce one output more than once and then block and wait, rather than blocking and waiting for one output and then producing another output. golang rocks!


Related articles: