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
158: Structured Logging with log/slog
Up until now, you've probably been using the standard log package. It's great for "quick and dirty" output, but the moment you move your code to a production environment with thousands of lines of logs, fmt.Printf-style logging becomes a liability. If you want to find every failed payment for a specific user ID across ten different microservices, you can't just grep through a wall of unstructured text efficiently.
That's where log/slog comes in. It brings structured logging to the standard library, allowing us to treat logs as data (usually JSON) rather than just strings. Let's build a small payment processing logger to see how this actually changes your workflow.
Starting with basic attributes
I want to track when a payment is processed. Instead of building a long string, I'll use slog.Info and pass key-value pairs. This allows a log aggregator (like ELK or Datadog) to index these fields individually.
package main
import (
"log/slog"
"os"
)
func main() {
orderID := "ORD-12345"
amount := 99.99
slog.Info("Processing payment",
"order_id", orderID,
"amount", amount,
)
}
Notice how I'm not using fmt.Sprintf. I'm providing a message, and then a sequence of keys and values. By default, this prints as text, but the structure is already there.
The mistake: Falling back into old habits
It's easy to slip back into the "string formatting" mindset. I actually did this in a project last week, and it's exactly what you want to avoid. Look at this:
// DON'T DO THIS
slog.Info(fmt.Sprintf("Payment %s failed for user %d", orderID, userID))
I realized a few minutes later that I'd just defeated the entire purpose of slog. By putting the variables inside the message string, I've made the "message" unique for every single log entry. Now, my log aggregator can't group all "Payment failed" events together because every message is slightly different. I have to fix it by moving those variables into attributes:
// DO THIS instead
slog.Error("Payment failed",
"order_id", orderID,
"user_id", userID,
)
Switching to JSON for the machines
While the default text output is readable for us humans during local development, servers prefer JSON. It's much faster for machines to parse. To switch, we need to create a Handler and a new Logger instance.
func main() {
// Create a JSON handler that writes to standard out
handler := slog.NewJSONHandler(os.Stdout, nil)
logger := slog.New(handler)
// Set this as the global logger so we don't have to pass it around
slog.SetDefault(logger)
slog.Info("System boot complete", "version", "1.0.4")
}
Now, instead of INFO System boot complete version=1.0.4, you get a clean JSON object. This is the gold standard for modern cloud applications.
Reducing repetition with logger groups
If I'm writing a whole module for "Payment Processing," I don't want to manually add "module", "payments" to every single log call. I can create a "pre-configured" logger that carries those attributes automatically using With().
func processPayment(id string) {
// Create a logger that always includes the module and the specific order ID
paymentLog := slog.Default().With(
"module", "payments",
"order_id", id,
)
paymentLog.Info("Validating card")
// ... some logic ...
paymentLog.Info("Charging account")
}
By using With(), I've created a child logger. Every time I call paymentLog.Info, it automatically attaches the module and order ID to the output. It keeps the actual log calls clean and focused only on what's happening in that specific moment.
📋 Practical Task
Build a Structured User Session Tracker
Create a small program that simulates a user session. Your task is to implement the following:
- Configure
log/slogto output in JSON format to the console. - Create a function called
trackEvent(userID string, eventName string, details map[string]any). - Inside that function, use a logger
With()to attach theuserIDas a persistent attribute. - Log the
eventNameas the main message, and iterate through thedetailsmap to add them as structured attributes to the log. - Test it by simulating a "login" event with
ip_addressandbrowserdetails, and a "purchase" event withitem_idandpricedetails.
There are no comments for now.