Logging Best Practices using slog in Go (Golang)*
With Working Code Examples
Logging is very important for understanding what happens in your application, especially when things go not as expected. Whether you are debugging, monitoring system health, or analyzing performance, a high-quality logs are a fundamental to have.
In this article we will walkthrough how to implement modern logging in Go using the standard log/slog package introduced in Go 1.21. You will learn the principles of structured logging, how to use log levels, how to include contextual information, and how to write logs to a file.
Why Not Just Use log.Println() ?
Go basic log.Println() is simple but limited:
log.Println("User signed in")Above approach lacks of the following:
Log levels (info, error, warning)
Structured data (key-value fields)
Context (user ID, request ID)
Integration with observability tools like Grafana, Elasticsearch, or Loki
When your application grows, unstructured logs become difficult to analyze, filter, or connect to specific issues.
Using log/slog: Structured Logging in Go
Since Go 1.21 , go introduce new package log/slog package, which provides:
Structured logging using key-value pairs
Log levels: Debug, Info, Warn, Error
Output handlers (text, JSON, etc.)
Better support for automation and observability platforms
Basic Example
package main
import (
"log/slog"
"os"
)
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
logger.Info("User login", "user_id", 42, "ip", "192.168.1.100")
}This produces structured logs in JSON format:
{"time":"2025-06-15T07:00:00Z","level":"INFO","msg":"User login","user_id":42,"ip":"192.168.1.100"}Structured logs like this are easy to index, filter, and analyze.
The Best Practices
1. Include Context in Logs
In real applications, you may want to add contextual data such as user ID, trace ID, or request information. This can be passed using context.Context.
package main
import (
"context"
"log/slog"
"os"
)
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
ctx := context.WithValue(context.Background(), "user_id", 42)
logWithContext(ctx, logger, "Payment processed")
}
func logWithContext(ctx context.Context, logger *slog.Logger, msg string) {
userID := ctx.Value("user_id")
logger.Info(msg, "user_id", userID)
}This will help you correlate logs across services and requests.
2. Use Proper Log Levels
Use appropriate log levels to indicate the severity of an event.
logger.Debug("Initializing payment gateway", "gateway", "Stripe")
logger.Info("User registered", "email", "user@example.com")
logger.Warn("Slow response", "duration", "3s")
logger.Error("Failed to connect to database", "error", err)This will help developers and operators filter and prioritize logs in production environments.
3. Write Logs to a File
To persist logs, so you can analyze them later, write logs to a file instead of just printing to the terminal.
package main
import (
"log/slog"
"os"
)
func main() {
logFile, err := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
panic("failed to open log file: " + err.Error())
}
defer logFile.Close()
logger := slog.New(slog.NewJSONHandler(logFile, nil))
logger.Info("Application started", "env", "production", "version", "1.0.0")
}You can now monitor app.log for activity:
tail -f app.log4. Write to Both Console and File
You can write logs to multiple destinations using io.MultiWriter.
package main
import (
"io"
"log/slog"
"os"
)
func main() {
file, err := os.OpenFile("combined.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
panic(err)
}
defer file.Close()
writer := io.MultiWriter(os.Stdout, file)
logger := slog.New(slog.NewJSONHandler(writer, nil))
logger.Info("Logging to both console and file")
}This is useful during development or when running locally while still collecting logs centrally.
5. Full Example: Logging in a REST API
Here is a complete example using Gin for HTTP handling and slog for structured logging.
package main
import (
"log/slog"
"net/http"
"os"
"github.com/gin-gonic/gin"
)
type CreateUserRequest struct {
Name string `json:"name" binding:"required,min=2"`
Email string `json:"email" binding:"required,email"`
Age int `json:"age" binding:"required,gte=0,lte=120"`
}
var logger *slog.Logger
func main() {
logFile, err := os.OpenFile("server.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
panic(err)
}
defer logFile.Close()
logger = slog.New(slog.NewJSONHandler(logFile, nil))
router := gin.Default()
router.POST("/users", createUserHandler)
logger.Info("Server starting on :8080")
router.Run(":8080")
}
func createUserHandler(c *gin.Context) {
var req CreateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
logger.Error("Validation failed", "error", err.Error())
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
logger.Info("User created",
"name", req.Name,
"email", req.Email,
"age", req.Age,
)
c.JSON(http.StatusOK, gin.H{"status": "user created"})
}Run it with:
go run main.goTest it using:
curl -X POST http://localhost:8080/users \
-H "Content-Type: application/json" \
-d '{"name":"Alice","email":"alice@example.com","age":30}'Check the server.log file for logs.
Le’s sum up
note: complete code can be retrieved here https://github.com/novrian6/go-slog.git


