📝 Logging in Go: Your Program’s Diary
The Story of the Detective’s Notebook
Imagine you’re a detective solving mysteries. Every time something happens—a door opens, a person enters, a clue appears—you write it down in your notebook. Why? Because later, when you need to figure out what went wrong (or right!), your notes tell the whole story.
That’s exactly what logging is for your Go programs!
Your program is like a busy restaurant kitchen. Things happen fast. Orders come in, dishes go out, sometimes things burn. Without a diary (logs), you’d never know what happened at 2:47 PM when Customer #42’s order went missing!
🎯 What You’ll Learn
- Basic Logging — Writing simple notes to track what your program does
- Structured Logging — Organized notes that computers (and humans) can search easily
Part 1: Basic Logging — Your First Diary Entries
What is Basic Logging?
Think of basic logging like writing in a simple diary:
“Monday 9am: Woke up. Had breakfast. Went to school.”
Simple. Easy. Gets the job done!
In Go, we use the log package that comes built-in. It’s like a free notebook that every Go program gets!
Your First Log Message
package main
import "log"
func main() {
log.Println("Hello! I started!")
log.Println("Doing some work...")
log.Println("All done! Goodbye!")
}
What happens? Your program writes three notes with timestamps:
2024/01/15 09:00:01 Hello! I started!
2024/01/15 09:00:01 Doing some work...
2024/01/15 09:00:01 All done! Goodbye!
See that date and time? Go adds it automatically, like magic! 🪄
The Three Musketeers of Basic Logging
Go gives you three main ways to write logs:
// 1. Print - Just writes a message
log.Print("Something happened")
// 2. Println - Writes message + new line
log.Println("Something happened")
// 3. Printf - Writes formatted message
name := "Alice"
log.Printf("User %s logged in", name)
Think of them like:
- Print = Writing on the same line
- Println = Writing and starting a new line
- Printf = Writing with blanks to fill in (like Mad Libs!)
When Things Go Wrong — Fatal and Panic
Sometimes things go SO wrong that your program can’t continue. Like a restaurant running out of all food!
// Fatal - Logs message, then STOPS program
log.Fatal("No database! Can't continue!")
// Panic - Logs message, then CRASHES (but can recover)
log.Panic("Something unexpected happened!")
⚠️ Warning: Use these only for SERIOUS problems. It’s like pulling the fire alarm—don’t do it for small stuff!
Saving Logs to a File
By default, logs appear on screen. But what if you want to save them forever? Like putting your diary in a safe!
package main
import (
"log"
"os"
)
func main() {
// Create a log file
file, err := os.Create("app.log")
if err != nil {
log.Fatal("Can't create log file!")
}
defer file.Close()
// Tell Go: "Write logs here!"
log.SetOutput(file)
log.Println("This goes to the file!")
}
Adding Extra Info to Logs
Want your logs to show more details? Like writing not just WHAT happened, but WHERE?
// Add file name and line number
log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
log.Println("Where am I?")
// Output: 2024/01/15 09:00:01 main.go:15: Where am I?
Here are the flags you can use:
| Flag | What it adds | Example |
|---|---|---|
Ldate |
Date | 2024/01/15 |
Ltime |
Time | 09:00:01 |
Lmicroseconds |
Tiny time details | .123456 |
Lshortfile |
File + line | main.go:15 |
Llongfile |
Full path + line | /home/user/main.go:15 |
Creating Your Own Logger
What if you want DIFFERENT diaries for different things? One for errors, one for regular stuff?
package main
import (
"log"
"os"
)
func main() {
// Error logger - shows [ERROR] prefix
errorLog := log.New(
os.Stderr,
"[ERROR] ",
log.Ldate|log.Ltime|log.Lshortfile,
)
// Info logger - shows [INFO] prefix
infoLog := log.New(
os.Stdout,
"[INFO] ",
log.Ldate|log.Ltime,
)
infoLog.Println("Starting app...")
errorLog.Println("Something went wrong!")
}
Output:
[INFO] 2024/01/15 09:00:01 Starting app...
[ERROR] 2024/01/15 09:00:01 main.go:20: Something went wrong!
Part 2: Structured Logging — The Smart Filing System
The Problem with Basic Logging
Imagine your diary has 10,000 pages. Someone asks: “Find all entries where Alice did something with error code 500.”
With basic logging, you’d have to read EVERY page! 😫
2024/01/15 09:00:01 User Alice got error 500 on /home
2024/01/15 09:01:23 Bob visited /about
2024/01/15 09:02:45 Error 404 for user Charlie
Good luck searching that mess!
Enter Structured Logging! 🦸♂️
Structured logging is like having a SUPER organized filing system. Instead of messy sentences, you get clean data:
{
"time": "2024-01-15T09:00:01Z",
"level": "error",
"user": "Alice",
"code": 500,
"path": "/home",
"message": "Page failed to load"
}
Now a computer can INSTANTLY find all entries where user = "Alice" AND code = 500!
Go’s Built-in slog Package (Go 1.21+)
Go now has a built-in structured logger called slog. It’s like getting a premium filing cabinet for free!
package main
import "log/slog"
func main() {
slog.Info("User logged in",
"user", "Alice",
"ip", "192.168.1.1",
)
}
Output:
2024/01/15 09:00:01 INFO User logged in user=Alice ip=192.168.1.1
See how organized that is? Each piece of info is labeled!
Log Levels — Importance Tags
Not all logs are equally important. Structured logging uses LEVELS:
slog.Debug("Tiny detail for developers")
slog.Info("Normal, everything's fine")
slog.Warn("Hmm, this might be a problem")
slog.Error("Something went wrong!")
Think of it like:
- Debug = 📝 Notes to yourself
- Info = ✅ “All good!”
- Warn = ⚠️ “Keep an eye on this…”
- Error = 🚨 “Something broke!”
JSON Output — Computers Love This!
Want your logs as pure JSON? Computers can read JSON super fast!
package main
import (
"log/slog"
"os"
)
func main() {
// Create JSON logger
logger := slog.New(
slog.NewJSONHandler(os.Stdout, nil),
)
logger.Info("User action",
"user", "Alice",
"action", "login",
"success", true,
)
}
Output:
{"time":"2024-01-15T09:00:01Z","level":"INFO","msg":"User action","user":"Alice","action":"login","success":true}
Text Output — Humans Love This!
Sometimes humans need to read logs too. Text format is friendlier:
logger := slog.New(
slog.NewTextHandler(os.Stdout, nil),
)
logger.Info("Server started", "port", 8080)
Output:
time=2024-01-15T09:00:01Z level=INFO msg="Server started" port=8080
Grouping Related Information
Sometimes you have related data. Group it!
slog.Info("Request completed",
slog.Group("user",
"id", 123,
"name", "Alice",
),
slog.Group("response",
"status", 200,
"time_ms", 45,
),
)
Output (JSON):
{
"msg": "Request completed",
"user": {"id": 123, "name": "Alice"},
"response": {"status": 200, "time_ms": 45}
}
Setting Minimum Log Level
Don’t want to see Debug messages in production? Set a minimum level!
opts := &slog.HandlerOptions{
Level: slog.LevelWarn, // Only Warn and Error
}
logger := slog.New(
slog.NewJSONHandler(os.Stdout, opts),
)
logger.Debug("You won't see me!")
logger.Info("Me neither!")
logger.Warn("But you'll see THIS!")
logger.Error("And definitely THIS!")
Adding Context That Sticks
Want some info to appear in ALL your logs? Use With:
// Create logger with default values
logger := slog.Default().With(
"service", "user-api",
"version", "1.2.3",
)
logger.Info("Starting...")
logger.Info("Stopping...")
Both messages will include service=user-api and version=1.2.3!
Logging Errors Properly
When errors happen, log them with context:
user := "Alice"
err := errors.New("database connection failed")
slog.Error("Failed to load user",
"user", user,
"error", err,
)
Output:
{
"level": "ERROR",
"msg": "Failed to load user",
"user": "Alice",
"error": "database connection failed"
}
🎭 Basic vs Structured: When to Use Each?
graph TD A["Need Logging?"] --> B{Small Script?} B -->|Yes| C["Basic Logging<br/>log package"] B -->|No| D{Need to Search<br/>Logs Later?} D -->|Yes| E["Structured Logging<br/>slog package"] D -->|No| C E --> F{Machine Processing?} F -->|Yes| G["JSON Handler"] F -->|No| H["Text Handler"]
| Situation | Use This |
|---|---|
| Quick script, learning | Basic log |
| Production app | Structured slog |
| Need to search logs | JSON format |
| Reading logs yourself | Text format |
| Debugging | slog.Debug() |
| Normal operations | slog.Info() |
| Potential problems | slog.Warn() |
| Actual errors | slog.Error() |
🚀 Real-World Example: Web Server Logging
Let’s put it all together with a mini web server:
package main
import (
"log/slog"
"net/http"
"os"
"time"
)
func main() {
// Setup logger
logger := slog.New(
slog.NewJSONHandler(os.Stdout, nil),
).With("service", "web")
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// Log the request
logger.Info("Request received",
"method", r.Method,
"path", r.URL.Path,
"ip", r.RemoteAddr,
)
w.Write([]byte("Hello!"))
// Log completion
logger.Info("Request completed",
"path", r.URL.Path,
"duration_ms", time.Since(start).Milliseconds(),
)
})
logger.Info("Server starting", "port", 8080)
http.ListenAndServe(":8080", nil)
}
🎯 Key Takeaways
-
Basic Logging (
logpackage)- Built into Go, no extra downloads
- Great for simple programs
- Use
Print,Println,Printffor messages - Use
FatalandPaniconly for serious errors - Customize with flags and custom loggers
-
Structured Logging (
slogpackage)- Organize data with key-value pairs
- Use log levels: Debug → Info → Warn → Error
- JSON for machines, Text for humans
- Group related data together
- Add persistent context with
With()
Remember: Good logs are like a good detective’s notebook. When something goes wrong at 3 AM, you’ll thank yourself for writing clear, organized logs! 🕵️♀️
Now you’re ready to make your Go programs talk! Next up: Play with logging in the interactive lab! 🎮
