The problem:
Amd's answer is essentially a state machine built with Go's select
statement. One problem I noticed is that when you add more functionalities (like "fast forward", "slow motion", etc.), more case
s have to be added to the select
in the "pause" case
.
Receiving from nil
channels:
In Go, receiving from (or sending to) a nil
channel results in "blocking forever". This in fact is a very important feature to implement the following trick: In a for
-select
pattern, if you set a case
channel
to nil
, the corresponding case
will not be matched in the next iteration. In other words, the case
is "disabled".
Receiving from closed channels:
In Go, receiving from a closed channel always returns immediately. Therefore, you may replace your default
case
by a variable holding a closed channel. When the variable holds the closed channel, it behaves like the default
case
; However, when the variable holds nil
, the case
is never matched, having the "pause" behavior.
My ideas:
- Modify your
default
case: read from a closed channel instead. (explained above);
- Make a backup of the closed channel. When
pause
is needed, set the "default case channel" to nil
; when play
is needed, set it to the backup;
- Make a "continue" channel to ask the
select
statement to re-read the variables after assignment;
- In fact, the "quit" channel can be reused as the "continue" channel: send
struct{}{}
when "continue" is needed; close()
when "quit" is needed;
- Encapsulate the resources in closures, and ensure that cleanup is done;
- Ensure that when
start()
is not yet called, no channels or go routines are created, in order to prevent leaks.
My implementation (also available at <kbd>The Go Playground</kbd>):
package main
import "fmt"
import "time"
import "sync"
func prepare() (start, pause, play, quit, wait func()) {
var (
chWork <-chan struct{}
chWorkBackup <-chan struct{}
chControl chan struct{}
wg sync.WaitGroup
)
routine := func() {
defer wg.Done()
i := 0
for {
select {
case <-chWork:
fmt.Println(i)
i++
time.Sleep(250 * time.Millisecond)
case _, ok := <-chControl:
if ok {
continue
}
return
}
}
}
start = func() {
// chWork, chWorkBackup
ch := make(chan struct{})
close(ch)
chWork = ch
chWorkBackup = ch
// chControl
chControl = make(chan struct{})
// wg
wg = sync.WaitGroup{}
wg.Add(1)
go routine()
}
pause = func() {
chWork = nil
chControl <- struct{}{}
fmt.Println("pause")
}
play = func() {
fmt.Println("play")
chWork = chWorkBackup
chControl <- struct{}{}
}
quit = func() {
chWork = nil
close(chControl)
fmt.Println("quit")
}
wait = func() {
wg.Wait()
}
return
}
func sleep() {
time.Sleep(1 * time.Second)
}
func main() {
start, pause, play, quit, wait := prepare()
sleep()
start()
fmt.Println("start() called")
sleep()
pause()
sleep()
play()
sleep()
pause()
sleep()
play()
sleep()
quit()
wait()
fmt.Println("done")
}
Extras:
If you really want to implement "fast forward" and "slow motion", simply:
- Refactor the magic
250
to a variable;
- Return one more closure from
prepare()
used to set the variable and send struct{}{}
to chControl
.
Please be reminded that "race conditions" are ignored for this simple case.
References:
https://golang.org/ref/spec#Send_statements
A send on a closed channel proceeds by causing a run-time panic. A send on a nil channel blocks forever.
https://golang.org/ref/spec#Receive_operator
Receiving from a nil channel blocks forever. A receive operation on a closed channel can always proceed immediately, yielding the element type's zero value after any previously sent values have been received.
https://golang.org/ref/spec#Close
Sending to or closing a closed channel causes a run-time panic. Closing the nil channel also causes a run-time panic. After calling close, and after any previously sent values have been received, receive operations will return the zero value for the channel's type without blocking. The multi-valued receive operation returns a received value along with an indication of whether the channel is closed.