Force priority of go select statement

The accepted answer has a wrong suggestion:

func sendRegularHeartbeats(ctx context.Context) {
    ticker := time.NewTicker(time.Second)
    defer ticker.Stop()

    for {
        //first select 
        select {
        case <-ctx.Done():
            return
        default:
        }

        //second select
        select {
        case <-ctx.Done():
            return
        case <-ticker.C:
            sendHeartbeat()
        }
    }
}

This doesn’t help, because of the following scenario:

  1. both channels are empty
  2. first select runs
  3. both channels get a message concurrently
  4. you are in the same probability game as if you haven’t done anything in the first select

An alternative but still imperfect way is to guard against concurrent Done() events (the “wrong select”) after consuming the ticker event i.e.

func sendRegularHeartbeats(ctx context.Context) {
    ticker := time.NewTicker(time.Second)
    defer ticker.Stop()

    for {            
        //select as usual
        select {
        case <-ctx.Done():
            return
        case <-ticker.C:
            //give priority to a possible concurrent Done() event non-blocking way
            select {
              case <-ctx.Done():
              return
            default:
            }
            sendHeartbeat()
        }
    }
}

Caveat: the problem with this one is that it allows for “close enough” events to be confused – e.g. even though a ticker event arrived earlier, the Done event came soon enough to preempt the heartbeat. There is no perfect solution as of now.

Leave a Comment