Error handling in Go multi process concurrent environment

introduction

In Go language, we usually use panic and recover to throw and catch errors. This pair of operations has no problem in a single coprocess, but in a multi coprocess, imagine the following two problems. Suppose we now have two processes:

  • If there is A panic in process A, will process B die because of the panic in process A?
  • If A panic occurs in coprocess A, can coprocess B use recover to capture the panic of coprocess A?

The answer is yes, No. The specific business scenarios are given below.

Question one

  • If there is A panic in process A, will process B die because of the panic in process A?

To verify this problem, we write a program:

package main

import (
    "fmt"
    "time"
)

func main() {

    go func() {
        for {
            fmt.Println("goroutine1_print")
        }
    }()

    go func() {
        time.Sleep(1 * time.Second)
        panic("goroutine2_panic")
    }()

    time.Sleep(2 * time.Second)
}

First, the main process starts two sub processes. Let's call them a and B. A. printing goroutine1 in a continuous cycle_ Print string; B will throw panic after sleeping for 1s (this step is to ensure that B will not panic until a runs). The main collaboration sleeps for 2s and waits for all the sub collaborations to be executed. The print results are as follows:

...
goroutine1_print
goroutine1_print
goroutine1_print
goroutine1_print
goroutine1_print
panic: goroutine2_panicgoroutine1_print


goroutine1_print
goroutine goroutine1_print
19goroutine1_print
goroutine1_print
goroutine1_print
goroutine1_print
 [runninggoroutine1_print
]:
goroutine1_print
goroutine1_print
goroutine1_print
main.main.func2()
        /Users/jiangbaiyan/go/src/awesomeProject/main.go:18 +0x46
created by main.main
        /Users/jiangbaiyan/go/src/awesomeProject/main.go:16 +0x4d

We can see that before the panic of coprocess B, coprocess a has been printing strings; Then coprocess A and panic print the string alternately, and finally the main coprocess and coprocess A and B all exit. Therefore, we can see that after a collaborative process panic, all collaborative processes will hang up, and the program will exit as a whole. Here we have verified the answer to the first question.
As for the reason why panic and coprocessor A print alternately, it may be that panic also needs to print strings. Because printing also takes time. When we execute the line of panic code, it takes A certain time (although the time is very short) for panic to really trigger all coroutines to hang up. Therefore, we will see the phenomenon of alternating printing in A short period of time.

Question two

  • If A panic occurs in coprocess A, can other coprocesses capture the panic of coprocess A with recover?

Similar to the above code, we can also simplify it:

package main

import (
   "fmt"
   "time"
)

func main() {

   defer func() {
       if e := recover(); e != nil {
           fmt.Println("recover_panic")
       }
   }()

   go func() {
       panic("goroutine2_panic")
   }()

   time.Sleep(2 * time.Second)
}

We only started one collaboration process this time, and added recover in the main collaboration process. We hope it can capture the panic in the sub collaboration process, but the result is not as desired:

panic: goroutine2_panic

goroutine 6 [running]:
main.main.func2()
       /Users/jiangbaiyan/go/src/awesomeProject/main.go:17 +0x39
created by main.main
       /Users/jiangbaiyan/go/src/awesomeProject/main.go:16 +0x57

Process finished with exit code 2

We can see that the recovery does not take effect. Therefore, we need to recover in which coordination process panic occurs. We change it to this:

package main

import (
   "fmt"
   "time"
)

func main() {

   go func() {
       defer func() {
           if e := recover(); e != nil {
               fmt.Println("recover_panic")
           }
       }()
       panic("goroutine2_panic")
   }()

   time.Sleep(2 * time.Second)
}

Results: recover was printed successfully_ Panic string:

recover_panic

Process finished with exit code 0

Therefore, our answer has also been verified: when a panic occurs in coprocess a, coprocess B cannot recover the panic of coprocess A. only the internal recovery of coprocess can capture the panic thrown by itself.

Best practices

Let's pretend to have such a scenario first. We want to develop a client. This client needs to call two services. These two services do not depend on any order, so we can start two goroutine s to request these two services concurrently. Then the problem becomes a problem at this time. Suppose there is a common logic in our business development. We usually don't want one service call to fail and the other service call to fail. What should we do at this time?
Smart, you will think that I will write a recover statement inside each collaboration process and let him catch the possible panic of each collaboration process, so as to solve the problem that a collaboration process panic causes all the collaboration processes to hang up. We write the following code, which is the best practice to solve problem 1 in combination with problem 2 in business development:

// When calling services concurrently, each handler will pass in a calling logic function
func GoroutineNotPanic(handlers ...func() error) (err error) {

    var wg sync.WaitGroup
    // Suppose we want to call so many services as handlers
    for _, f := range handlers {

        wg.Add(1)
        // Each function starts a coroutine
        go func(handler func() error) {

            defer func() {
                // Each coroutine uses recover internally to capture the panic that may occur in the calling logic
                if e := recover(); e != nil {
                    // If an error is reported in a service call collaboration, you can print some error logs here
                }
                wg.Done()
            }()

            // Take the handler with the first error to call the logic, and finally return to the outside
            e := handler()
            if err == nil && e != nil {
                err = e
            }
        }(f)
    }

    wg.Wait()

    return
}

Tags: Go Back-end goroutine

Posted by SUNNY ARSLAN on Sat, 21 May 2022 18:14:01 +0300