First separate the waiting on go-routines and the done channel.
Use a sync.WaitGroup to coordinate the goroutines.
func main() {
wait := &sync.WaitGroup{}
N := 3
wait.Add(N)
for i := 1; i <= N; i++ {
go goFunc(wait, i, true)
}
wait.Wait()
fmt.Println(`Exiting main`)
}
Each goroutine will look like this:
// code for the actual goroutine
func goFunc(wait *sync.WaitGroup, i int, closer bool) {
defer wait.Done()
defer fmt.Println(`Exiting `, i)
T := time.Tick(time.Duration(100*i) * time.Millisecond)
for {
select {
case <-T:
fmt.Println(`Tick `, i)
if closer {
return
}
}
}
}
(https://play.golang.org/p/mDO4P56lzBU)
Our main func is successfully waiting for the goroutines to exit before it exits. Each goroutine is closing itself, and we want a way to cancel all our goroutines at the same time.
We'll do this with a chan, and make use of this feature of receiving from channels:
QUOTE: 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#Receive_operator)
We modify our goroutines to check for a CLOSE:
func goFunc(wait *sync.WaitGroup, i int, closer bool, CLOSE chan struct{}) {
defer wait.Done()
defer fmt.Println(`Exiting `, i)
T := time.Tick(time.Duration(100*i) * time.Millisecond)
for {
select {
case <-CLOSE:
return
case <-T:
fmt.Println(`Tick `, i)
if closer {
close(CLOSE)
}
}
}
}
and then we change our func main so that it passes the CLOSE channel through, and we'll set the closer variable so that only the last of our goroutines will trigger the close:
func main() {
wait := &sync.WaitGroup{}
N := 3
CLOSE := make(chan struct{})
// Launch the goroutines
wait.Add(N)
for i := 1; i <= N; i++ {
go goFunc(wait, i, i == N, CLOSE)
}
// Wait for the goroutines to finish
wait.Wait()
fmt.Println(`Exiting main`)
}
(https://play.golang.org/p/E91CtRAHDp2)
Now it looks like everything is working.
But it isn't. Concurrency is hard. There's a bug lurking in this code, just waiting to bite you in production. Let's surface it.
Change our example so that every goroutine will close:
func main() {
wait := &sync.WaitGroup{}
N := 3
CLOSE := make(chan struct{})
// Launch the goroutines
wait.Add(N)
for i := 1; i <= N; i++ {
go goFunc(wait, i, true /*** EVERY GOROUTINE WILL CLOSE ***/, CLOSE)
}
// Wait for the goroutines to finish
wait.Wait()
fmt.Println(`Exiting main`)
}
Change goroutine so that it takes a while before closing. We want two goroutines to be about to close at the same time:
// code for the actual goroutine
func goFunc(wait *sync.WaitGroup, i int, closer bool, CLOSE chan struct{}) {
defer wait.Done()
defer fmt.Println(`Exiting `, i)
T := time.Tick(time.Duration(100*i) * time.Millisecond)
for {
select {
case <-CLOSE:
return
case <-T:
fmt.Println(`Tick `, i)
if closer {
/*** TAKE A WHILE BEFORE CLOSING ***/
time.Sleep(time.Second)
close(CLOSE)
}
}
}
}
(https://play.golang.org/p/YHnbDpnJCks)
We crash with:
Tick 1
Tick 2
Tick 3
Exiting 1
Exiting 2
panic: close of closed channel
goroutine 7 [running]:
main.goFunc(0x40e020, 0x2, 0x68601, 0x430080)
/tmp/sandbox558886627/prog.go:24 +0x2e0
created by main.main
/tmp/sandbox558886627/prog.go:38 +0xc0
Program exited: status 2.
While a receive on a closed channel returns immediately, you cannot close a closed channel.
We need a little coordination. We can do this with a sync.Mutex and a bool to indicate whether we've closed the channel or not. Let's create a struct to do this:
type Close struct {
C chan struct{}
l sync.Mutex
closed bool
}
func NewClose() *Close {
return &Close {
C: make(chan struct{}),
}
}
func (c *Close) Close() {
c.l.Lock()
if (!c.closed) {
c.closed=true
close(c.C)
}
c.l.Unlock()
}
Rewrite our gofunc and our main to use our new Close struct, and we're good to go:
https://play.golang.org/p/eH3djHu8EXW
The problem with concurrency is that you always need to be wondering what would happen if another 'thread' was anywhere else in the code.