0

I have three goroutines which depend on each other, and I am coordinating them with channels. Two of the routines are writers and one is a reader to a named pipe.

However, each of these routines could have an error. For example, os.OpenFile could fail in several of these routines. If this happens, we want to exit the routine immediately. However, if we do that, then the other routines will block because the channels will never be consumed.

How do I coordinate and handle error states across these three routines? Do I need an error channel for every single action, and then interleave checking them in every other routine?

func main() {
    syscall.Mkfifo("p.pipe", 0666)
    writer, _ := os.Create("output.csv")

    queryDone := make(chan error)
    waitingDone := make(chan error)
    copyDone := make(chan error)
    go func() {
        // Open for reading and copy to http handler
        r, _ := os.OpenFile("p.pipe", os.O_RDONLY, os.ModeNamedPipe)
        n, _ := io.Copy(writer, r)
        e := <-waitingDone
        r.Close()
        copyDone <- e
    }()

    go func() {
        // Open for writing
        pipe, _ := os.OpenFile("p.pipe", os.O_WRONLY|os.O_APPEND, os.ModeNamedPipe)
        e := <-queryDone
        _ = pipe.Close()
        waitingDone <- e
    }()

    go func() {
        // DB query
        db, _ := sql.Open("duckdb", "")
        defer db.Close()
        _, err = db.Exec("COPY (select 1) TO 'p.pipe' (FORMAT CSV)")
        queryDone <- err
    }()

    e := <-copyDone
    fmt.Println("Done", e)
}
2
  • 1
    remove as much complexity from the goroutines as you can. I would suggest creating the resources then passing them into the goroutines.
    – JimB
    Commented Mar 12 at 15:30
  • @JimB in my specific case, that is tough, since OpenFile() to write to a named pipe actually blocks until another process opens for reading.
    – poundifdef
    Commented Mar 12 at 15:39

1 Answer 1

1

Use a cancelable context, and cancel it if error happens:

ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Channel to capture the first error
errCh:=make(chan error,1)

// Template for goroutines
go func() {
   for {
     err:=doSometing()
     if err!=nil {
        // Record the error in the channel
        select {
           case errCh<-err:
           default:
        }
        // cancel all goroutines
        cancel()
        return
     }
     if ctx.Err()!=nil {
         return // Error happened somewhere else
     }
}()
...
// Check if error happened. This will collect the first error
var err error
select {
  case err=<-errCh:
  default:
}
if err!=nil {
   // Handle error
}

There are also third part libraries that can help with coordinating error state among multiple goroutines.

2
  • So my other routines should check for ctx.Done() ? Are there particular libraries I should look at?
    – poundifdef
    Commented Mar 12 at 15:40
  • Take a look at pkg.go.dev/golang.org/x/sync/errgroup. Yes, all goroutines should check ctx.Done() or ctx.Err() Commented Mar 12 at 16:11

Not the answer you're looking for? Browse other questions tagged or ask your own question.