"Errors are values" embodies a core design philosophy in Go's error handling system, setting it apart from many other languages.
Imagine a car:
Other languages(Exception-Based): A car's engine suddenly explodes, and the car stops immediately. You have no choice but to address the engine problem before you can proceed.
Go(Error as Value): A car's low fuel light comes on, but the engine keeps running. You can make a choice: stop for gas, or try to reach a gas station. The car's performance is impacted, but you can control your options.
Why Go Rocks:
- Control: You, as the developer, have total control over how errors are handled, preventing code execution from crashing abruptly.
- Explicit Decisions: Every error is explicitly checked. You are forced to consider what actions should be taken (try again, log it, recover or exit).
- Simple and Elegant: Error values fit within Go's clean syntax and contribute to clean code.
1. The error
Type
In go, errors are represented using the built-in error
type.
It is an interface with a single method.
Example:
type error interface {
Error() string
}
Any type that implements the Error()
method qualifies as an error.
2. Custom Errors
fmt.Errorf
is used to create an error. The function may return both the result and the error.The error message should start with small caps.
Example:
package main
import (
"fmt"
)
func divide(a, b int) (int, error) {
if b == 0 {
return 0, fmt.Errorf("cannot divide %d by %d", a, b)
}
return a / b, nil
}
3. Handling Errors
When the function returns two values(result, err), you can explicitly check the second value; err != nil
and if there's an error, you can handle it.
Example:
package main
import (
"fmt"
)
func main() {
result, err := divide(10, 0)
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("Result:", result)
}
}
4. Custom Error Types
You can create a custom error by defining a struct that implements the error
interface. Learn about interfaces here
Example:
package main
import (
"fmt"
)
type DivideByZeroError struct {
message string
}
func (e *DivideByZeroError) Error() string{
return e.Message
}
func customDivide(a, b int) (int, error) {
if b == 0 {
return 0, &DivideByZeroError{"cannot divide by zero"}
}
return a / b, nil
}
5. Wrapping and Unwrapping Errors
Since release of Go 1.13, the errors
package provides utilities for wrapping and unwrapping errors, enabling adding more context to errors.
-
fmt.Errorf
is used with %w to wrap the original error for context. -
errors.Unwrap
extracts the original error wrapped with another error.
Example:
package main
import (
"fmt"
"errors"
)
func openFile(filename string) error {
return fmt.Errorf("openFile: failed to read file %s: %w",filename, errors.New("file not found"))
}
func main() {
err := openFile("test.txt")
if err != nil {
if unwrappedErr := errors.Unwrap(err); unwrappedErr != nil {
fmt.Println("Original error:", unwrappedErr)
}
fmt.Println("Error:", err)
}
}
6. Panic
You can handle severe errors using panic
. This is for unexpected situations that your program cannot recover from. When a function panics, it stops execution and begins unwinding the stack, running deferred functions in reverse order.
Example:
package main
import "fmt"
func doPanic() {
panic("something went wrong")
}
func main() {
fmt.Println("Starting...")
doPanic()
fmt.Println("This will never be printed")
}
7. Handling Panic
You can recover from a panic by using defer and recover.
Defer : The defer keyword schedules a function to be executed after the surrounding function returns, or in case of a panic, before the stack unwinds. Deferred functions clean up resources or handle any final tasks.
Example:
package main
import "fmt"
func cleanup() {
fmt.Println("Clean up done.")
}
func riskyOperation() {
defer cleanup()
panic("Something went wrong")
}
func main() {
riskyOperation()
fmt.Println("Main finished") // This will not be printed because of the panic
}
In this example, the cleanup function is deferred and will run before the program crashes, but the "Main finished" message will never be printed because the panic stops the normal flow.
Recover: recover is used to catch a panic and prevent it from terminating the program. It is typically used in a deferred function.
Example:
package main
import "fmt"
func riskyOperation() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered from panic:", r)
}
}()
panic("A critical failure")
}
func main() {
riskyOperation()
fmt.Println("Main continues normally after recovering from panic")
}
In this case, the recover function catches the panic and the program can continue running after the panic.
8. Error Handling Best Practices:
- Always check for errors: In Go, ignoring errors is possible but discouraged.
- Return wrapped errors for context: This helps in debugging by providing more detail about the error path.
- Avoid using
panic
for regular error handling: It's better to return errors using the error type and let the caller decide how to handle them. - Libraries should not use
panic
for expected errors: Instead, they should return error values, allowing the client code to handle them.
Conclusion
Go's error handling approach helps developers like you write reliable and easy-to-maintain programs. It gives you the flexibility to manage errors in a way that fits your application, leading to clearer and more robust code.
Top comments (3)
So many ways to handle errors, I'm really impressed by the custom error part.
yes,error handling is go's forte. Custom errors allow you to add more context , this is a huge plus when debugging. Glad you enjoyed @denilany
nice article