2. Go语言日志基础:标准库log包的使用、日志级别(Debug/Info/Warn/Error)的概念
日志是分布式系统的“眼睛”。在构建链路追踪体系之前,必须掌握Go语言最基础的日志输出能力。本章从标准库 log 包入手,讲解其核心用法,并引入日志级别的概念,为后续结构化日志与链路追踪打下基础。
2.1 标准库log包:快速上手
Go标准库的 log 包提供了最简洁的日志输出功能,无需任何第三方依赖即可使用。它默认输出到标准错误(os.Stderr),并自动添加时间戳。
2.1.1 基本输出函数
| 函数 | 行为 | 典型场景 |
|---|---|---|
log.Print() |
输出日志,末尾自动换行 | 常规信息记录 |
log.Printf() |
格式化输出日志 | 带变量的日志消息 |
log.Println() |
输出日志,参数间自动加空格并换行 | 多字段拼接输出 |
log.Fatal() |
输出日志后调用 os.Exit(1) |
致命错误,程序无法继续 |
log.Panic() |
输出日志后触发 panic |
不可恢复的运行时错误 |
关键区别:
Fatal 直接终止进程,Panic 会触发 defer 栈执行后再终止。在分布式服务中,Fatal 通常只用于初始化阶段,运行时错误应使用 Panic 或自定义错误处理。
package main
import (
"log"
)
func main() {
// 基础输出
log.Print("服务启动中...")
log.Printf("监听端口: %d", 8080)
log.Println("初始化完成", "耗时:", "2.3s")
// 致命错误示例(实际使用中应谨慎)
// log.Fatal("数据库连接失败")
// panic示例
// log.Panic("无法恢复的错误")
}
2.1.2 自定义Logger
标准库允许创建独立的Logger实例,实现输出目标、前缀和格式的定制。这在微服务多模块场景中尤为重要。
package main
import (
"io"
"log"
"os"
)
func main() {
// 创建文件日志输出
file, _ := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
defer file.Close()
// 同时输出到文件和控制台
multiWriter := io.MultiWriter(file, os.Stdout)
// 自定义Logger:前缀 + 日期时间 + 文件名行号
logger := log.New(multiWriter, "[MyService] ", log.Ldate|log.Ltime|log.Lshortfile)
logger.Println("这是自定义Logger的输出")
logger.Printf("请求ID: %s, 耗时: %dms", "req-123", 45)
}
| 标志常量 | 含义 |
|---|---|
log.Ldate |
本地时区日期:2009/01/23 |
log.Ltime |
本地时区时间:01:23:23 |
log.Lmicroseconds |
微秒精度时间:01:23:23.123123 |
log.Llongfile |
完整文件名 + 行号:/a/b/c/d.go:23 |
log.Lshortfile |
短文件名 + 行号:d.go:23 |
log.LUTC |
使用UTC时间而非本地时间 |
log.Lmsgprefix |
将前缀移到消息正文之前(Go 1.14+) |
最佳实践: 在分布式系统中,建议始终开启
log.Lshortfile 或 log.Llongfile,这能帮助你在海量日志中快速定位代码位置。同时使用 io.MultiWriter 实现日志的“双写”——既写入文件持久化,又输出到控制台实时观察。
2.2 日志级别的概念与实现
标准库 log 包本身不提供日志级别(Debug/Info/Warn/Error)的支持。但在实际项目中,不同环境需要过滤不同级别的日志:开发环境输出所有级别,生产环境只输出 Warn 及以上级别。理解日志级别的概念,是后续使用第三方日志库(如 zap、logrus)的基础。
2.2.1 日志级别金字塔
2.2.2 各级别定义与使用场景
| 级别 | 数值(常见约定) | 含义 | 典型场景 |
|---|---|---|---|
| DEBUG | 0 或 -1 | 调试信息,开发阶段使用 | 变量值打印、函数进入/退出、请求参数详情 |
| INFO | 1 | 常规信息,记录系统正常运行状态 | 服务启动、请求开始/结束、配置加载 |
| WARN | 2 | 警告信息,表示潜在问题但不影响当前功能 | 重试次数超过阈值、配置即将过期、慢查询 |
| ERROR | 3 | 错误信息,功能受到影响但服务仍可运行 | 数据库连接失败、下游服务超时、业务校验失败 |
常见误区: 不要将 ERROR 级别用于“业务预期内的异常”。例如用户输入密码错误,应使用 INFO 或 WARN 记录,而非 ERROR。ERROR 应保留给系统级故障,如磁盘空间不足、第三方服务不可用等。
2.3 基于标准库实现简易分级日志
虽然标准库没有内置级别,但我们可以通过封装 log.Logger 快速实现一个支持级别过滤的日志工具。这个模式在小型项目或中间件开发中非常实用。
package main
import (
"io"
"log"
"os"
)
// LogLevel 定义日志级别
type LogLevel int
const (
DEBUG LogLevel = iota
INFO
WARN
ERROR
)
// Logger 封装标准库日志,支持级别过滤
type Logger struct {
level LogLevel
logger *log.Logger
}
// NewLogger 创建分级日志实例
func NewLogger(out io.Writer, level LogLevel, prefix string) *Logger {
return &Logger{
level: level,
logger: log.New(out, prefix, log.Ldate|log.Ltime|log.Lshortfile),
}
}
func (l *Logger) Debug(format string, v ...interface{}) {
if l.level <= DEBUG {
l.logger.Printf("[DEBUG] "+format, v...)
}
}
func (l *Logger) Info(format string, v ...interface{}) {
if l.level <= INFO {
l.logger.Printf("[INFO] "+format, v...)
}
}
func (l *Logger) Warn(format string, v ...interface{}) {
if l.level <= WARN {
l.logger.Printf("[WARN] "+format, v...)
}
}
func (l *Logger) Error(format string, v ...interface{}) {
if l.level <= ERROR {
l.logger.Printf("[ERROR] "+format, v...)
}
}
func main() {
// 生产环境:只输出 WARN 及以上级别
prodLogger := NewLogger(os.Stdout, WARN, "[Prod] ")
prodLogger.Debug("这条不会输出") // 被过滤
prodLogger.Info("这条也不会输出") // 被过滤
prodLogger.Warn("磁盘使用率超过80%%")
prodLogger.Error("数据库连接超时")
// 开发环境:输出所有级别
devLogger := NewLogger(os.Stdout, DEBUG, "[Dev] ")
devLogger.Debug("请求参数: %+v", map[string]string{"key": "value"})
devLogger.Info("服务启动成功")
}
2.4 标准库日志的局限性
在进入链路追踪之前,必须正视标准库 log 包的不足。这些局限性正是后续章节引入结构化日志和第三方库的原因。
| 局限性 | 说明 | 对链路追踪的影响 |
|---|---|---|
| 无日志级别 | 需要手动封装,且无法动态调整 | 无法按级别过滤追踪日志,增加存储成本 |
| 非结构化输出 | 纯文本格式,难以被日志系统解析 | 无法自动提取 traceID、spanID 等字段 |
| 全局锁竞争 | 所有输出共享一把互斥锁 | 高并发下成为性能瓶颈,影响追踪采样 |
| 无上下文传递 | 无法携带请求级别的上下文信息 | 无法自动关联日志与追踪链路 |
| 缺乏异步支持 | 同步写入,阻塞调用方 | 在高吞吐追踪中增加延迟 |
学习路径提示: 本章使用标准库是为了让你理解日志的“底层原理”。从下一章开始,我们将引入
zap 和 logrus 等高性能结构化日志库,它们原生支持级别、JSON 输出、上下文绑定,是构建分布式链路追踪日志体系的基石。
2.5 日志输出目标与旋转
在分布式系统中,日志不能只输出到控制台。标准库的 log.SetOutput() 可以重定向输出目标,但日志文件的“旋转”(按大小或时间切割)需要额外处理。
2.5.1 输出到文件
package main
import (
"log"
"os"
)
func main() {
// 以追加模式打开日志文件
file, err := os.OpenFile("server.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
log.Fatalf("无法打开日志文件: %v", err)
}
defer file.Close()
// 全局重定向
log.SetOutput(file)
log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
log.Println("所有日志将写入 server.log 文件")
}
2.5.2 日志旋转策略对比
| 策略 | 触发条件 | 优点 | 缺点 |
|---|---|---|---|
| 按大小旋转 | 文件达到指定大小(如 100MB) | 控制单个文件体积,便于传输 | 无法按时间维度归档 |
| 按时间旋转 | 到达指定时间间隔(如每天) | 按时间维度清晰归档 | 流量低谷时文件可能很小 |
| 混合策略 | 大小或时间任一条件满足 | 兼顾体积和时间维度 | 实现复杂度较高 |
生产建议: 标准库本身不支持日志旋转。在生产环境中,推荐使用
lumberjack 或 file-rotatelogs 等第三方库,或者依赖系统工具(如 logrotate)进行外部旋转。在后续章节中,我们将演示如何将 zap 与 lumberjack 集成。
2.6 本章小结
- 标准库
log包提供Print、Fatal、Panic三类基础输出函数,支持自定义 Logger 和输出目标。 - 日志级别分为 DEBUG、INFO、WARN、ERROR 四级,生产环境通常只保留 WARN 及以上级别。
- 通过封装标准库可以实现简易分级日志,但存在无结构化、全局锁、无上下文传递等局限性。
- 日志旋转是生产环境的刚需,标准库不直接支持,需要借助第三方库或系统工具。
- 理解标准库的不足,是学习高性能结构化日志库和链路追踪日志体系的前提。