Why does this OrDone Channel Implementation receive twice from Done Channel?

Viewed 205

Been reading through Concurrency in Go and it introduces a handy "or-done" channel.

TLDR; when you're working with a channel that you're not in control of (presumably from some other part of your system), the code can get a little ugly.

// Quite nice to read
for v := range myChan {
  ... Do Stuff
}

// Not so nice
loop:
for {
  select {
  case <- done:
    break loop
  case maybeVal, ok := <-myChan:
    if !ok {
      return
    }
    // Do something with maybeVal
  }
}

The book offers a way to simplify this with the OrDone channel. Defined as follows. What I don't understand is why, in the nested select, we need to again receive from <-done.

orDone := func(done, c <-chan interface{}) <-chan interface{} {
  valStream := make(chan interface{})
  go func() {
    defer close(valStream)
    for {
      select {
      case <- done:
        return
      case v, ok := <-c:
        if !ok {
          return
        }
        select {
        case valStream <- v:
        case <-done: // Why do we also need to receive on done here?
        }
      }
    }
  }
  return valStream
}

This allows you to go back to your original for loop, enhancing readability - like so:

for val := range orDone(done, myChan) {
  // Once again, do something
}
3 Answers

Really just adding visibility to Peter's answer.

It's because valStream itself may block the send if whoever is receiving valStream loses interest.

In a situation where you have received a value from channel c you enter into your nested select{}. At this point if we took out the second <-done you would have this;

select {
case valStream: <- v:
}

This will block indefinitely until a value is received from channel v even if the done channel is closed. By adding a nested check, we allow ourselves to exit the select{} at either point.

An additional question, why is the body of that "case <-done:" empty?

orDone := func(done, c <-chan interface{}) <-chan interface{} {
  valStream := make(chan interface{})
  go func() {
    defer close(valStream)
    for {
      select {
      case <- done:
        return
      case v, ok := <-c:
        if !ok {
          return
        }
        select {
        case valStream <- v:
        case <-done: 
            return  // I think there should be a "return" here
        }
      }
    }
  }
  return valStream
}
Related