Error handling is the process of responding to and resolving error conditions in a program. In Go, error handling is done using the error
type, which is a built-in interface that represents an error condition.
The error
type is a simple interface that has a single method, Error() string
, which returns a string representation of the error. When a function returns an error, it is the caller's responsibility to check for the error and handle it appropriately.
The code above demonstrates how to handle errors in Go. The mayReturnError
function returns an error if the shouldError
parameter is true. In the main
function, we check for the error using an if
statement and print the error message if it exists.
Panic is a built-in function in Go that stops the normal execution of a goroutine. When a panic occurs, the program will print the panic message and the stack trace, and then it will terminate. Panic is typically used for unrecoverable errors, such as accessing a nil pointer or an out-of-bounds array index.
If we don't check for errors, we may end up with unexpected behavior or panic in our program. For example, if we try to access a nil pointer or divide by zero, our program will panic and crash.
Compiled from Go Error Handling, now we move into the error type. In Go, errors are represented by the error
interface type. Any value that implements this interface can be used as an error, as long as it can provide a string description of itself. The definition of this interface is quite simple:
type error interface {
Error() string
}
Like other built-in types in Go, the error interface is predefined in the universe block, making it available throughout your code without imports. The errors package provides the most widely used implementation of this interface through its internal errorString
type.
// errorString is a trivial implementation of error.
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
The errors.New function provides a way to create an error value by converting a provided string into an errors.errorString
instance, which is then returned as an error interface.
// New returns an error that formats as the given text.
func New(text string) error {
return &errorString{text}
}
Here’s how you might use errors.New:
When you call Sqrt
with a negative number, you receive a non-nil error (specifically an errors.errorString
instance). This error message ("math: square root of...") can be retrieved either by invoking the error's Error
method or simply printing the error directly:
fmt.Println(Sqrt(-2.0))
Assume you're working on an API that connects to a database. You do multiple operations, such as creating, reading, updating, and deleting records. Each of these operations can fail for various reasons, such as network issues or invalid input. Most of the time this operations runs one after another. So you need to handle errors in a way that allows you abort the operation if one of them fails because your data integrity is at stake.
// UpdateStock updates stock for multiple products
// in a transaction
func (sm *StockManager) UpdateStock(orderItems map[int]int) error {
// Begin transaction
tx, err := db.Begin()
if err != nil {
return fmt.Errorf("failed to begin transaction: %w", err)
}
// If function returns with error, rollback the transaction
// Otherwise, commit the transaction
defer func() {
if err != nil {
tx.Rollback()
log.Println("Transaction rolled back")
}
}()
// Check and update stock for each product in the order
for productID, quantity := range orderItems {
// First check if we have enough stock
var currentStock int
err = tx.QueryRow(`
SELECT
stock
FROM
products
WHERE
id = ?
`, productID).Scan(¤tStock)
if err != nil {
return fmt.Errorf("error checking stock for product %d: %w",
productID, err)
}
if currentStock < quantity {
return fmt.Errorf("insufficient stock for product %d", productID)
}
// Update the stock
_, err = tx.Exec(`
UPDATE
products
SET
stock = stock - ?
WHERE
id = ?
`, quantity, productID)
if err != nil {
return fmt.Errorf("error updating stock for product %d: %w",
productID, err)
}
log.Printf("Updated stock for product %d: %d → %d\n",
productID, currentStock, currentStock-quantity)
}
// If we reach here, all operations succeeded, so commit
// the transaction
if err := tx.Commit(); err != nil {
return fmt.Errorf("failed to commit transaction: %w", err)
}
log.Println("Transaction committed successfully")
return nil
}
In this example, we use a transaction to ensure that all stock updates are atomic. If any operation fails, we roll back the transaction to maintain data integrity. The defer
statement ensures that the rollback occurs if an error is encountered. More about this, read it in the Execute Transactions article.
package main
import (
"errors"
"fmt"
)
func main() {
var err error // err is nil
if err != nil {
fmt.Println("Error:", err)
}
if err := errors.New("oh no!"); err != nil {
// This creates a new 'err' variable that shadows the outer one
fmt.Println("Error:", err)
}
if err == nil {
fmt.Println("No error!")
} else {
fmt.Println("Error:", err)
}
}
Error handling is a crucial aspect of programming in Go. By using the error
type and the panic
function, you can effectively manage errors and ensure that your programs behave predictably. Remember to always check for errors and handle them appropriately to avoid unexpected behavior or crashes in your applications.