SoFunction
Updated on 2025-03-05

Simple simulation of Python generator using Go language

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

gen = demo_input_and_output()
print(())
print((42))

This code demonstrates the functionality of python generator. You can see that yield does two operations at the same time. One is to send data out "waht is the input", and the operation at the same time is to collect data inward. Moreover, this data receiving operation is a blocking operation. If the external call next() (that is, pass None inward) or call send(42) (that is, pass the value 42 inward), then the blocking operation will continue to wait.

In other words, python generator comes with an external communication channel for sending and receiving messages. This is what I wrote if I simulate a python generator using go

Copy the codeThe code is as follows:
package main

import "fmt"

func demoInputAndOutput(channel chan string) {
    channel <- "what is my input?"
    input := <- channel
    channel <- ("input is: %s", input)
}

func main() {
    channel := make(chan string)
    go demoInputAndOutput(channel)
    (<- channel)
    channel <- "42"
    (<- channel)
}

This code is basically equivalent to the python version. The implicit channel becomes explicit in the go version. yield becomes a channel <- operation, and immediately performs a <- channel blocking read operation. This is the essence of yield.

The channel of go can also be used for loop as an iterator:

Copy the codeThe code is as follows:
package main

import "fmt"

func someGenerator() <-chan string {
    channel := make(chan string)
    go func() {
        channel <- "a"
        ("after a")
        channel <- "c"
        ("after c")
        channel <- "b"
        ("after b")
        close(channel)
    }()
    return channel
}

func main() {
    channel := someGenerator()
    for val := range channel {
        (val)
    }
}

Unlike python's yield, the channel <- here is not equivalent to yield, it will execute downward until it blocks. The effect is

Copy the codeThe code is as follows:
after a
a
c
after c
after b
b

This is different from the expected order. The reason why the channel has not printed out after a after c after b is that the channel has only one element buffer by default, so it blocks after writing one. If the buffer is increased, then it will be effective

Copy the codeThe code is as follows:
make(chan string, 10)

The output becomes:

after a
after c
after b
a
c
b

It can be seen that goroutine is like an independent thread playing with itself without waiting for execution. If you want to simulate yield, you need to add the displayed synchronization operation (block the read signal from the channel):

Copy the codeThe code is as follows:
package main

import "fmt"

func someGenerator() chan string {
    channel := make(chan string)
    go func() {
        channel <- "a"
        <- channel
        ("after a")
        channel <- "c"
        <- channel
        ("after c")
        channel <- "b"
        <- channel
        ("after b")
        close(channel)
    }()
    return channel
}

func main() {
    channel := someGenerator()
    for val := range channel {
        (val)
        channel <- ""
    }
}

The result of the output is

a
after a
c
after c
b
after b

From here we can see that the python generator is like golang's goroutine with a bufferless channel. This results in a coroutine context switching every time a value is yielded. Although coroutine context switching is cheap, it is not without cost. Designs like the buffered channel of goroutine can allow a goroutine to generate more output at one time and then block and wait, instead of blocking and waiting for one output, and then generating another output. golang rocks!