Logging in Go Applications: Best Practices for Beginners
Logging is not just about writing messages to the console. In production, logs help track errors, analyze performance, and understand system behavior. In this article, we will cover the best practices for logging in Go (Golang) for beginners: from the standard library to modern structured approaches.
Why is Logging Important in Go?
Go is a language for building high-load server applications. Without quality logging, you will not be able to:
- Quickly locate bugs in a production environment
- Understand the sequence of events during a failure
- Conduct a security audit
- Optimize performance
Good logging should be: structured, leveled (debug, info, warn, error), contextual (contain request ID, function name), and manageable (rotation, file output).
1. Starting with the Standard Library log
Go provides a built-in log package — simple and sufficient for small projects. Here is how it works:
package main
import ( "log" "os")
func main() { // Basic setup: output to stderr log.SetOutput(os.Stderr) log.SetPrefix("INFO: ") log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
log.Println("Application started") log.Printf("User %s authorized", "admin") log.Fatal("Critical error") // terminates the program with code 1}Key features of log:
log.Println()— string outputlog.Printf()— formatted outputlog.Fatal()— output + program terminationlog.Panic()— output + panic
But the standard logger has drawbacks: no levels (debug, info, error), no structured output (JSON), no file rotation.
2. Adding Logging Levels
For production applications, it is critical to separate logs by level. Here is a minimal implementation using the standard package:
package main
import ( "log" "os")
type Logger struct { debug *log.Logger info *log.Logger warn *log.Logger error *log.Logger}
func NewLogger() *Logger { flags := log.Ldate | log.Ltime | log.Lshortfile return &Logger{ debug: log.New(os.Stdout, "DEBUG: ", flags), info: log.New(os.Stdout, "INFO: ", flags), warn: log.New(os.Stderr, "WARN: ", flags), error: log.New(os.Stderr, "ERROR: ", flags), }}
func (l *Logger) Debug(msg string, args ...interface{}) { l.debug.Printf(msg, args...)}
func (l *Logger) Info(msg string, args ...interface{}) { l.info.Printf(msg, args...)}
func (l *Logger) Error(msg string, args ...interface{}) { l.error.Printf(msg, args...)}
func main() { log := NewLogger() log.Info("Server started on port %d", 8080) log.Debug("Request from %s", "192.168.1.1") log.Error("Failed to connect to DB: %v", "timeout")}Tip: In production, use os.Stderr for errors and os.Stdout for regular logs — this simplifies log collection via systemd or Docker.
3. Structured Logging (JSON)
In modern systems, logs are often collected in centralized storage (Elasticsearch, Loki). For this, structured output is needed, for example in JSON. Popular libraries:
- logrus — flexible, with formatter support
- zap — ultra-fast, from Uber
- zerolog — lightweight and fast
Example with logrus:
package main
import ( "os" "gi