/
main.go
95 lines (88 loc) · 3.21 KB
/
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
package main
import (
"fmt"
"time"
"github.com/davecgh/go-spew/spew"
)
// The goroutine has a few paths to termination:
// • When it has completed its work.
// • When it cannot continue its work due to an unrecoverable error.
// • When it’s told to stop working
/**
We get the first two paths for free—these paths are your algorithm—but what about
work cancellation? This turns out to be the most important bit because of the net‐
work effect: if you’ve begun a goroutine, it’s most likely cooperating with several other
goroutines in some sort of organized fashion.
**/
func main() {
resourceLeak()
cancellationSignal()
}
// Here we see that the main goroutine passes a nil channel into doWork. Therefore, the
// strings channel will never actually gets any strings written onto it, and the goroutine
// containing doWork will remain in memory for the lifetime of this process (we would
// even deadlock if we joined the goroutine within doWork and the main goroutine).
// In this example, the lifetime of the process is very short, but in a real program, gorou‐
// tines could easily be started at the beginning of a long-lived program. In the worst
// case, the main goroutine could continue to spin up goroutines throughout its life,
// causing creep in memory utilization.
func resourceLeak() {
doWork := func(strings <-chan string) <-chan interface{} {
completed := make(chan interface{})
go func() {
defer fmt.Println("doWork exited.")
defer close(completed)
for s := range strings {
// Do something interesting
fmt.Println(s)
}
}()
return completed
}
doWork(nil)
// Perhaps more work is done here
fmt.Println("Done.")
}
// The way to successfully mitigate this is to establish a signal between the parent gorou‐
// tine and its children that allows the parent to signal cancellation to its children. By
// convention, this signal is usually a read-only channel named done. The parent gorou‐
// tine passes this channel to the child goroutine and then closes the channel when it
// wants to cancel the child goroutine. Here’s an example:
func cancellationSignal() {
// Here we pass the done channel to the doWork function. As a convention, this channel is the first parameter.
doWork := func(
done <-chan interface{},
strings <-chan string,
) <-chan interface{} {
terminated := make(chan interface{})
go func() {
defer fmt.Println("doWork exited.")
defer close(terminated)
for {
select {
case s := <-strings:
fmt.Println(s)
// On this line we see the ubiquitous for-select pattern in use. One of our case statements
// is checking whether our done channel has been signaled. If it has, we return from the goroutine.
case t := <-done:
spew.Dump(t)
return
}
}
}()
return terminated
}
done := make(chan interface{})
terminated := doWork(done, nil)
// Here we create another goroutine that will cancel the goroutine spawned in
// doWork if more than one second passes.
go func() {
// Cancel the operation after 1 second.
time.Sleep(1 * time.Second)
fmt.Println("Canceling doWork goroutine...")
close(done)
}()
// This is where we join the goroutine spawned from doWork with the main goroutine.
<-terminated
fmt.Println("Done.")
}