title: Goroutine cancellation with recovery
body: <p>Without frameworks like RxGo, how could I accomplish the following in Go?:</p>
  <ul>
  <li>Three goroutines of different running times are in a context: short, medium, and, long. These goroutines are long-running jobs like reading or uploading a large file.</li>
  <li>If any one of the three goroutines cancel (on error), the other two will cancel.</li>
  <li>Run code on all three goroutines when cancelled (like panic &amp; recover). These are actions like, closing the large file or notifying the upload has failed.</li>
  </ul>
  <p>In a diagram, the desired outcome:</p>
  <p><a href="https://i.stack.imgur.com/eq1Lz.png" rel="nofollow noreferrer"><img src="https://i.stack.imgur.com/eq1Lz.png" alt="enter image description here" /></a></p>
  <p>Updated attempt:</p>
  <pre><code>package main
  
  import (
      &quot;context&quot;
      &quot;fmt&quot;
      &quot;math/rand&quot;
      &quot;sync&quot;
      &quot;time&quot;
  )
  
  func randInt(min int, max int) int {
      return min + rand.Intn(max-min)
  }
  
  func main() {
  
      ctx := context.Background()
      ctx, cancel := context.WithCancel(ctx)
  
      rand.Seed(time.Now().UnixNano())
      wg := &amp;sync.WaitGroup{}
      errworker := make(chan int, 4)
  
      work := func(i int, ec chan int, c context.Context, cx context.CancelFunc) {
          defer wg.Done()
  
          workchan := make(chan struct{})
  
          go func() {
              interval := time.Duration(randInt(1, 10000)) * time.Millisecond
              fmt.Printf(&quot;Operation %d started: will take %s\n&quot;, i, interval)
              time.Sleep(interval)
  
              if randInt(0, 100) &gt; 80 {
                  fmt.Printf(&quot;Operation %d failed!\n&quot;, i)
                  ec &lt;- i
                  cx()
              }
              workchan &lt;- struct{}{}
          }()
  
          select {
          case &lt;-workchan:
              fmt.Printf(&quot;Operation %d done\n&quot;, i)
          case &lt;-c.Done():
              fmt.Printf(&quot;Operation %d halted\n&quot;, i)
          }
      }
  
      for i := 0; i &lt; 4; i++ {
          wg.Add(1)
          go work(i, errworker, ctx, cancel)
      }
  
      wg.Wait()
      close(errworker)
      for e := range errworker {
          fmt.Printf(&quot;Error in worker %d\n&quot;, e)
      }
  }
  
  </code></pre>
  <p>(Edit) Playground: <a href="https://go.dev/play/p/CNACYe43Dh3" rel="nofollow noreferrer">https://go.dev/play/p/CNACYe43Dh3</a></p>
  <p>Updated attempt playground: <a href="https://go.dev/play/p/5lzdERwqG8o" rel="nofollow noreferrer">https://go.dev/play/p/5lzdERwqG8o</a></p>
  <p>Is there a simple, elegant ELI5 solution to this problem? I can't help but notice with channels, I may need child contexts with child goroutines to listen for the cancellation signal, since when a goroutine enters a long running job, there was no way (from what I've tried) to stop the goroutine other than cancelling the context altogether. A controlled way to terminate the goroutines early is desired.</p>
  <p><strong>(Edit)</strong>
  In my updated attempt I simply placed the actual work in an inner goroutine while the outer goroutine listens for context completion. In this attempt I decided to use an error channel (although in theory only one error will come through) to catch and process errors at the end.</p>
  <p>Are there any caveats or blind spots to this approach? Feel free to correct me on implementation and approach, the goal is to have a controlled way to terminate goroutines early.</p>
  <p>Thanks!</p>