goroutine关闭频道`done'后不打印`shutdown message`

Two go routines reading from the same channel. The first go routine never prints its shutdown message after the done channel is closed, while the second go routine always does.

Why is the message from the first go routine not printing and is the method even returning?

main.go

func main() {
done := make(chan bool)
c := make(chan os.Signal, 1)

cameras := client.CameraConfig()
client.DrawUserControls(cameras)

operator := client.NewOperator(cameras)
go operator.UserInputListener(done)
go operator.ParseAndExecuteUserCommand(done)

signal.Notify(c, os.Interrupt)
for range c {
    close(done)
    break
}

log.Println("Interrupt signal received. Shutting client down....")
time.Sleep(5 * time.Second)
}

client.go

func (o *Operator) UserInputListener(done <-chan bool) {
    reader := bufio.NewReader(os.Stdin)
    for {
        select {
        case <-done:
            log.Println("Keyboard listener shutting down.") // <-- this never prints
            return
        default:
            line, _, err := reader.ReadLine()
            if err != nil {
                log.Println(err)
            }

            data := strings.Split(string(line), "")

            id, err := strconv.Atoi(data[1])
            if err != nil {
                log.Println(err)
                continue
            }

            switch data[0] {
            case "b":
                o.Controls <- Ctrl{
                    Identifier: id,
                    Ctrl:       "run",
                }
            case "t":
                o.Controls <- Ctrl{
                    Identifier: id,
                    Ctrl:       "terminate",
                }
            case "r":
                o.Controls <- Ctrl{
                    Identifier: id,
                    Ctrl:       "record",
                }
            case "s":
                o.Controls <- Ctrl{
                    Identifier: id,
                    Ctrl:       "stop",
                }
            }
        }
    }
}

func (o *Operator) ParseAndExecuteUserCommand(done <-chan bool) {
    for {
        select {
        case <-done:
            log.Println("Command operator shutting down.")
            return
        case ctrl := <-o.Controls:
            switch ctrl.Ctrl {
            case "run":
                o.Room[ctrl.Identifier].Run()
            case "terminate":
                o.Room[ctrl.Identifier].Close()
            case "record":
                o.Room[ctrl.Identifier].Write()
            case "stop":
                o.Room[ctrl.Identifier].Stop()
            }
        }
    }
}

The reason is because you have created synchronous channel and you push here 1 message and then you could read it only once as well. That is because you get only 1 (random) read from done channel.

The way you can shut down your goroutines is to use WaitGroup:
main.go:

var (
    done            chan bool
)

func main() {
    cameras := client.CameraConfig()
    client.DrawUserControls(cameras)
    operator := client.NewOperator(cameras)

    done = make(chan bool, 1)
    wg := &sync.WaitGroup{}
    wg.Add(2)
    go operator.UserInputListener(done, wg)
    go operator.ParseAndExecuteUserCommand(done, wg)

    handleShutdown()
    wg.Wait()
}


func handleShutdown() {
    ch := make(chan os.Signal, 1)
    go func() {
        <-ch //wait for application terminating
        log.Println("Shutdown received.")
        close(done)
    }()
    signal.Notify(ch, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP)
}

client.go:

func (o *Operator) UserInputListener(done <-chan bool, wg *sync.WaitGroup) {
    defer wg.Done()
    for {
        select {
        case <-done:
            log.Println("Keyboard listener shutting down.") 
            return
        ........
        }
    }
}

func (o *Operator) ParseAndExecuteUserCommand(done <-chan bool, wg *sync.WaitGroup) {
    defer wg.Done()
    for {
        select {
        case <-done:
            log.Println("Command operator shutting down.") 
            return
        ........
        }
    }
}

Use this link for details

The reason UserInputListener is not entering case <-done: is because it is stuck waiting for some input at this line:

line, _, err := reader.ReadLine()

This line is blocking!

What is the solution for this type of problem?

This is not easy.

Read Routine

You could use another go routine to do the reading, send the data into a channel that you read from in the select where you also read from the done channel. That would properly close the UserInputListener but leave the other goroutine to not be closed properly. But maybe that does not matter as much...?

func (o *Operator) UserInputListener(done <-chan bool) {
    // channel with some buffer so reader doesn't have to wait (so much)
    ch := make(chan string, 10)
    go func() {
        reader := bufio.NewReader(os.Stdin)
        for {
            line, _, err := reader.ReadLine()
            if err != nil {
                log.Println(err)
                // stop on error?
                // return
            }
            ch <- string(line)
        }
    }()

    for {
        select {
        case <-done:
            log.Println("Keyboard listener shutting down.") // <-- this never prints
            return
        case line:= <-ch:
            data := strings.Split(line, "")

            id, err := strconv.Atoi(data[1])
            if err != nil {
                log.Println(err)
                continue
            }

            switch data[0] {
            case "b":
                o.Controls <- Ctrl{
                    Identifier: id,
                    Ctrl:       "run",
                }
            case "t":
                o.Controls <- Ctrl{
                    Identifier: id,
                    Ctrl:       "terminate",
                }
            case "r":
                o.Controls <- Ctrl{
                    Identifier: id,
                    Ctrl:       "record",
                }
            case "s":
                o.Controls <- Ctrl{
                    Identifier: id,
                    Ctrl:       "stop",
                }
            }
        }
    }
}

Close Reader Source

You could also try to close whatever the reader is reading from. I have already used this solution in other contexts like reading from serial devices.

This does not work with os.Stdin however as JimB pointed out to me. os.StdIn.Close() will block as it waits for the reader to finish.