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 日志级别金字塔

日志级别金字塔 ERROR WARN INFO DEBUG 级别越高,严重程度越高,输出量越少 生产环境通常设置为 WARN 级别,过滤 DEBUG 和 INFO

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 及以上级别。
  • 通过封装标准库可以实现简易分级日志,但存在无结构化、全局锁、无上下文传递等局限性。
  • 日志旋转是生产环境的刚需,标准库不直接支持,需要借助第三方库或系统工具。
  • 理解标准库的不足,是学习高性能结构化日志库和链路追踪日志体系的前提。