In Go, functions return errors as a second (or final) return value, and you must explicitly check them using if err != nil before proceeding. Ignoring an error is a compile-time error unless you intentionally assign it to the blank identifier _.
Here is the standard pattern for defining a function that returns an error and the caller handling it:
// Define a function that returns a value and an error
func Divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("division by zero")
}
return a / b, nil
}
func main() {
result, err := Divide(10, 0)
// Always check the error immediately
if err != nil {
// Handle the error: log, return, or panic depending on context
fmt.Printf("Error occurred: %v\n", err)
return // Stop execution if the error is fatal
}
// Safe to use result only if err is nil
fmt.Printf("Result: %v\n", result)
}
For I/O operations or library calls, you often chain error checks. If you need to wrap an error to add context (Go 1.13+), use fmt.Errorf with the %w verb:
func ReadConfig(path string) error {
data, err := os.ReadFile(path)
if err != nil {
// Wrap the original error with context
return fmt.Errorf("failed to read config file %s: %w", path, err)
}
// Process data...
return nil
}
Key practices to remember:
- Check immediately: Do not defer error checking until the end of a function; handle it right after the call to prevent using invalid data.
- Return early: If an error occurs, return immediately from the function to avoid deep nesting of
if/elseblocks. - Use
errors.Isanderrors.As: When checking for specific error types or sentinel errors, use these standard library functions instead of comparing error strings or types directly.
if errors.Is(err, os.ErrNotExist) {
// Handle specific "file not found" case
}
This explicit error handling model forces developers to acknowledge failure cases, making Go applications more robust and predictable compared to languages that rely on exceptions.