尝试使用close(ch)结束goroutine,但最终无限运行

I am trying to end multiple goroutines once another goroutine closes a channel. However, I am ending up into infinite loop after close signal is received. I can't figure out why.

I know that it is possible using context.Context but I was trying out by closing channels.

Go Playground: https://play.golang.org/p/C6pcYgGLnG9

package main

import (
    "fmt"
    "time"
    "sync"
)


func runner(id int, ch <-chan struct{}, wg *sync.WaitGroup) {
    for {
        select {
            case <-time.Tick(time.Second):
                fmt.Println("worker ", id)
            case <- ch:
                fmt.Println("closing worker ", id)
                break
        }
    }

    wg.Done()

}

func main() {
    fmt.Println("Hello, playground")
    ch := make(chan struct{})
    var wg sync.WaitGroup

    wg.Add(1)
    go runner(1, ch, &wg)

    wg.Add(1)
    go runner(2, ch, &wg)

    time.Sleep(5*time.Second)

    close(ch)

    wg.Wait()
}

The problem is the scope of your break:

func runner(id int, ch <-chan struct{}, wg *sync.WaitGroup) {
    for {
        select {
            case <-time.Tick(time.Second):
                fmt.Println("worker ", id)
            case <- ch:
                fmt.Println("closing worker ", id)
                break
        }
    }

    wg.Done()
}

You want to break out of the for loop, but you're actually only breaking out of the select. To remedy this, you have two choices:

  1. Add a label to your for loop, and break from it explicitly:

    func runner(id int, ch <-chan struct{}, wg *sync.WaitGroup) {
    loop:  // <---------- add a label
        for {
            select {
            case <-time.Tick(time.Second):
                fmt.Println("worker ", id)
            case <-ch:
                fmt.Println("closing worker ", id)
                break loop  // <---------- and break from it explicitly
            }
        }
    
        wg.Done()
    }
    
  2. Probably a more idiomatic and robust solution, simply return when you're done. This means the wg.Done() call must be deferred.

    func runner(id int, ch <-chan struct{}, wg *sync.WaitGroup) {
        defer wg.Done() // <--- Defer the wg.Done() call, so it happens on return
        for {
            select {
            case <-time.Tick(time.Second):
                fmt.Println("worker ", id)
            case <-ch:
                fmt.Println("closing worker ", id)
                return   // <--- replace `break` with `return`
            }
        }
    }