Search code examples
selectgoclientchat

Golang - use two Reader returned lines for select statement


For a simple golang chat/telnet client, I want to pass strings received from two bufio Readers to a select statement, so I can either send the user input to the server, or print the server sent data.

conn, _ := net.Dial("tcp", "localhost:8998")
for {
    select{
    case line, _ := bufio.NewReader(os.Stdin).ReadString('\n'):
        fmt.Print("> ")
        fmt.Fprintf(conn, line + "\n")
    case data, _ := bufio.NewReader(conn).ReadString('\n'):
        fmt.Print(data)
    }
}

The compiler gives me back this error

select case must be receive, send or assign recv

I suspect I should be using channels. But

conn, _ := net.Dial("tcp", "localhost:8998")
outgoing := make(chan string)
incoming := make(chan string)
for {
    inputReader := bufio.NewReader(os.Stdin)
    connReader := bufio.NewReader(conn)

    o, _ := inputReader.ReadString('\n')
    i, _ := connReader.ReadString('\n')
    outgoing <- o
    incoming <- i

    select{
    case out := <-outgoing:
        fmt.Print("> ")
        fmt.Fprintf(conn, out + "\n")
    case in := <-incoming:
        fmt.Print(in)
    }
}

But the code doesn't accept or receive data. Finally, I suspect I should be using two go routines to check for the Reader return value?


Solution

  • You need to run them in goroutines or they can't simultaneously occur:

    conn, _ := net.Dial("tcp", "localhost:8998")
    
    // Make outgoing reader routine
    outgoing := make(chan string)
    go func() {
        inputReader := bufio.NewReader(os.Stdin)
        for {
            o, err := inputReader.ReadString('\n')
            if err != nil {
                fmt.Printf("outgoing error: %v", err)
                return
            }
            outgoing <- o
        }
    }()
    
    // Make incoming reader routine
    incoming := make(chan string)
    go func() {
        connReader := bufio.NewReader(conn)
        for {
            i, err := connReader.ReadString('\n')
            if err != nil {
                fmt.Printf("incoming error: %v", err)
                return
            }
            incoming <- i
        }
    }()
    
    for {
        select {
        case out := <-outgoing:
            fmt.Print("> ")
            fmt.Fprintf(conn, out+"\n")
        case in := <-incoming:
            fmt.Print(in)
        }
    }
    

    Edit

    To further elaborate: your first example won't work because select requires each case to be either a channel read or channel send operation, and the function call bufio.Reader.ReadString() is neither.

    Your second example won't work because the line outgoing <- o will block indefinitely. It is trying to send o over the channel outgoing, but outgoing is not buffered, and nothing is listening on it. Furthermore, since neither ReadString() call will return until a line is read, your for loop will only proceed once a line is read from BOTH readers in turn (and will then block on the channel send).

    That's why you need each Reader to have its own goroutine, so each can independently read off its Reader as input allows, and store it to the relevant channel as lines are read, and your select statement can then react to each line as it comes in from the individual goroutines.