Error Handling in Go#
Created: December 7, 2021 6:24 PM
Tags: Golang
Published: December 29, 2021
Problem#
One of the most annoying issues in the Go language is error handling.
Every function needs to return an error, and each return requires a check, resulting in a significant portion of code dedicated to error handling. Moreover, the code for handling errors is often redundant and repetitive. As someone who has been influenced by Python's philosophy of simplicity, this is a great source of pain for me.
Fortunately, this problem can be optimized. Of course, better readability may sacrifice some performance, but considering that Go already has excellent performance and readability is a significant concern, reducing the amount of error handling code in complex logic areas can be more beneficial than harmful.
Solution#
Let's demonstrate with an example:
// Let's start with some code
// Function that handles string comparison
func A(a, b string) (string, error) {
if a != b {
return "", errors.NewErr("info")
}
return a, nil
}
// Function that handles integer comparison
func B(a, b int) (int, error) {
if a != b {
return 0, errors.NewErr("info")
}
return a, nil
}
// Main function
func Main(a, b string, c, d int) error {
// In the conventional approach, error handling is required for each function call
res, err := A(a, b)
if err != nil {
return err
}
res, err = B(c, d)
if err != nil {
return err
}
// 100 more function calls are omitted here
return nil
}
From the above code, we can see that for each function call in the main function, error handling is required. This leads to the logic of the function call being overshadowed by error handling.
Our solution is to modify the called functions to handle errors as well, moving the error handling from the main function to each individual function. This way, the more a function is reused, the more code is saved. Additionally, readability is greatly improved.
Let's demonstrate with the modified code:
func A(a, b string, err error) (string, error) {
// Added an "err" parameter and the following logic
// If the error parameter is not nil, return it
if err != nil {
return "", err
}
if a != b {
return "", errors.NewErr("info")
}
return a, nil
}
func B(a, b int) (int, error) {
if err != nil {
return 0, nil
}
if a != b {
return 0, errors.NewErr("info")
}
return a, nil
}
func Main(a, b string, c, d int) error {
res, err := A(a, b, nil)
res, err = B(c, d, err)
// 100 more function calls are omitted here
// It can be observed that regardless of the number of function calls, error handling is only written once
if err != nil {
return err
}
return nil
}
Reflection#
Can this approach be used universally to solve our error handling problem?
I believe that this is not a universal solution. It has at least the following issues:
- Although subsequent functions are not actually executed once an error occurs in one of the functions, there is still function call and parameter passing overhead, resulting in some performance loss. This approach is not suitable for parts where extreme performance is desired.
- This approach may increase the difficulty of debugging because the error information is returned only after executing all the logic. Therefore, it cannot directly show the specific location of the error. However, there are solutions to this issue as well. We can include function stack information in the error message, but it adds some complexity.
Conclusion#
This error handling approach can greatly improve code readability, but it may have a slight impact on performance and add some difficulty to debugging. However, for complex code, I personally believe that the trade-off of sacrificing a bit of performance for higher readability is worth it.