Go
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions and Methods
-
Section 4: Concurrency
-
Section 5: Packages and Tooling
-
Section 6: More Standard Library
-
Section 7: Building Services
-
Section 8: Advanced Go
-
Section 9: Go in the Cloud-Native Ecosystem
-
Section 10: Data Structures and Algorithms in Go
-
Section 11: Testing and Deployment
-
Section 12: Practical Projects
-
Section 13: More Standard Library Practice
-
Section 14: More Practice Projects
-
Section 15: Design Patterns in Go
-
Section 16: Interview Practice
-
Section 17: Package fmt In Depth
-
Section 18: Package strings and strconv
-
Section 19: Package os and io
-
Section 20: Package time
-
Section 21: Package sort and container
-
Section 22: Package encoding
-
Section 23: Package net/http In Depth
-
Section 24: Package context
-
Section 25: Package regexp and bytes
-
Section 26: Package errors In Depth
-
Section 27: Package crypto and hash
-
Section 28: Package flag and log
-
Section 29: Package sync In Depth
-
Section 30: More Practice Exercises
-
Section 31: Go Modules and Workspaces In Depth
-
Section 32: Generics Deep Dive (Go 1.18+)
-
Section 33: Testing Package In Depth
-
Section 34: More Interview and Whiteboard Practice
-
Section 35: Package math and unicode
-
Section 36: Package path and filepath
-
Section 37: Package database/sql
-
Section 38: Package text/template and html/template
-
Section 39: Package archive and compress
-
Section 40: Lower-Level net Package
-
Section 41: Package runtime and reflect
-
Section 42: Package embed and io/fs
-
Section 43: Go Assembly and CGO Basics
-
Section 44: Building CLIs and TUIs
-
Section 45: Go Performance Tuning
-
Section 46: More Real-World Projects
-
Section 47: Go in Production
-
Section 48: Go Design Patterns
-
Section 49: Go Interfaces Deep Dive
-
Section 50: Final Practice and Review
24: Error Handling Idioms
Imagine you're running a professional kitchen. You don't just throw all the ingredients in a pot, turn on the heat, and walk away for an hour, hoping for the best. If you did, and the sauce broke or the meat burned, you wouldn't know exactly when or why it happened—you'd just have a ruined dinner. Instead, you taste the food at every single stage. You check the sear on the steak; you taste the reduction. If the sauce is too salty, you handle it right then—either by adding a potato to absorb the salt or by starting over—before that mistake moves down the line to the plating station.
Go treats errors exactly like that kitchen workflow. We don't use "try-catch" blocks to wrap a giant chunk of code and hope we catch whatever flies out. Instead, we "taste" for errors after almost every single operation. It feels repetitive at first, but it means you never lose track of exactly where a failure happened.
Stop the Bleeding Early
In Go, an error is just a value. When a function can fail, it returns that error as its last return value. Your job is to check it immediately. I've seen a lot of beginners try to ignore this or push the error handling to the end of the function, but that's a recipe for disaster. You want to handle the error as close to the source as possible.
type User struct {
ID int
Name string
}
func FetchUser(id int) (*User, error) {
// Imagine this calls a database
if id == 0 {
return nil, errors.New("invalid user ID")
}
return &User{ID: id, Name: "Alice"}, nil
}
func ProcessUser() error {
user, err := FetchUser(0)
if err != nil {
// We "taste" the error immediately.
// We don't proceed to use 'user' because it's nil.
return fmt.Errorf("could not process user: %v", err)
}
fmt.Println("Processing", user.Name)
return nil
}
Adding Context to the Trail
If you just return err as-is, your logs will eventually be filled with generic messages like "file not found". When your app is running in production, "file not found" is useless. Which file? Why were we looking for it? This is where wrapping comes in.
By using fmt.Errorf with the %w verb, you wrap the original error. It's like adding a note to the dish: "The sauce is too salty (because the sous-chef added double the salt)." You keep the original error for the computer to check, but add a human-readable trail for yourself.
func LoadConfig(path string) error {
_, err := os.Open(path)
if err != nil {
// %w wraps the error, allowing us to unwrap it later
return fmt.Errorf("failed to load config from %s: %w", path, err)
}
return nil
}
Distinguishing the Flavor of Failure
Sometimes, your code needs to behave differently depending on why something failed. For example, if a user isn't found, you might return a 404; if the database is down, you return a 500. For this, we use Sentinel Errors—predefined variables that act as constants.
To check for these, don't use == if you've wrapped your errors. Use errors.Is. It recursively unwraps the error chain to see if the sentinel is hiding inside.
var ErrNotFound = errors.New("resource not found")
func GetRecord(id string) error {
if id == "" {
return fmt.Errorf("search failed: %w", ErrNotFound)
}
return nil
}
func HandleRequest(id string) {
err := GetRecord(id)
if errors.Is(err, ErrNotFound) {
fmt.Println("Sending 404 to client...")
} else if err != nil {
fmt.Println("Sending 500 to client...")
}
}
If you need to access specific fields from a custom error struct (like an error code or a retry-after timestamp), use errors.As. It's essentially a type-safe cast that looks through the wrapped layers to find a specific error type. I personally find this much cleaner than the old-school type assertion err.(*MyCustomError) because it handles wrapped errors automatically.
📋 Practical Task
Build a Robust Order Validation System
You are building a simplified checkout system. Your goal is to implement a validation chain that distinguishes between different types of failures using Go's error idioms.
Requirements:
- Define a sentinel error called
ErrOutOfStock. - Define a custom error struct called
ValidationErrorthat includes aFieldstring (e.g., "Email" or "ZipCode") and the original error. - Create a function
ValidateInventory(item string) errorthat returnsErrOutOfStockif the item is "PS5", otherwise returnsnil. - Create a function
ValidateShipping(zip string) errorthat returns aValidationErrorif the zip code is empty. - Create a main
ProcessOrderfunction that calls both validations. If a validation fails, it must wrap the error with the context"order processing failed"using%w. - In your
mainfunction, callProcessOrderand useerrors.Isto detect if the failure was specifically due to stock issues, anderrors.Asto detect if it was a validation error so you can print which specific field caused the problem.
There are no comments for now.