Skip to content

《Go Cookbook CN》系列 04:日志记录核心方法与实践全解

约 2815 字大约 9 分钟

《Go Cookbook CN》系列Go语言

2026-04-02

本文聚焦Go语言标准库中log包的核心使用方法,从日志记录的基础认知出发,逐步拆解日志写入、格式定制、文件输出、日志级别划分及系统日志服务集成的全流程实操方案。通过本文的学习,你将掌握Go程序中日志记录的核心技巧,能够根据实际场景灵活配置日志输出方式与格式,解决日志管理中的常见问题。

本篇核心收获

  • 理解Go语言log包的三类核心日志写入函数(Print/Fatal/Panic)的差异及适用场景
  • 掌握自定义标准logger输出内容(日期、时间、文件行号等)的配置方法
  • 实现日志输出到文件、同时输出到屏幕+文件的实操方案
  • 学会基于log包构建多级别日志体系,并通过环境变量控制日志输出范围
  • 掌握将Go程序日志接入系统syslog服务的配置与编码方法

2 日志记录基础认知

2.1 日志记录的价值与风险

日志记录是程序运行过程中记录事件的核心手段,其价值与风险并存:

  • 核心价值:程序故障排查时,日志是回溯事件序列、定位根因的关键诊断资源;同时可用于实时监控程序运行状态,触发异常警报。
  • 潜在风险
    1. 性能开销:日志写入会占用处理器资源,高负载/性能敏感场景下可能影响程序性能;
    2. 存储风险:若未配置日志清理/轮换策略,日志文件持续增长可能占满磁盘空间,甚至导致服务器崩溃。

2.1 模块小结

日志记录是程序调试与运维的核心能力,需在“记录足够信息”与“控制性能/存储开销”之间做好平衡。

3 Go log包核心使用方法

Go标准库log包提供了开箱即用的日志记录能力,默认将日志写入标准错误输出(如命令行窗口),并为每条日志添加时间戳。该包的核心函数分为3组,各自适用于不同场景。

3.1 三类核心日志写入函数

3.1.1 Print:仅记录日志,不终止程序

Print组包含Print/Printf/Println,仅将日志写入logger,程序会继续执行,与fmt.Println的唯一区别是日志带时间戳。

示例代码

func main() {
    str := "abcdefghi"
    num, err := strconv.ParseInt(str, 10, 64)
    if err != nil {
        log.Println("Cannot parse string:", err)
    }
    fmt.Println("Number is", num)
}

执行结果

% go run main.go
2022/01/23 18:39:06 Cannot parse string: strconv.ParseInt: parsing "abcdefghi": invalid syntax
Number is 0

3.1.2 Fatal:记录日志后终止程序

Fatal组包含Fatal/Fatalf/Fatalln,写入日志后会调用os.Exit(1)终止程序,退出码1表示程序异常。

示例代码

func main() {
    str := "abcdefghi"
    num, err := strconv.ParseInt(str, 10, 64)
    if err != nil {
        // 将消息写入到标准 logger,然后退出程序,退出码为 1
        log.Fatal("Cannot parse string", err)
    }
    // 当程序发生问题时,本行代码不会被执行
    fmt.Println("Number is", num)
}

执行结果

% go run main.go
2022/01/23 18:42:10 Cannot parse string strconv.ParseInt: parsing "abcdefghi": invalid syntax
exit status 1

3.1.3 Panic:记录日志后触发panic

Panic组包含Panic/Panicf/Panicln,写入日志后调用panic():停止当前goroutine→运行延迟代码→向上冒泡panic,最终导致程序退出(退出码2,Go 1.17.6仍未修复该退出码语义不符的问题)。

示例代码

func conv(s string) int64 {
    defer fmt.Println("deferred code in function conv")
    n, err := strconv.ParseInt(s, 10, 64)
    if err != nil {
        // 将日志写入到标准logger,然后引发panic
        log.Panic("Cannot parse string", err)
    }
    return n
}

func main() {
    str := "abcdefghi"
    num := conv(str)
    fmt.Println("Number is", num)
}

执行结果

% go run main.go
2022/01/23 18:48:20 Cannot parse string strconv.ParseInt: parsing "abcdefghi": invalid syntax
deferred code in function conv
panic: Cannot parse string strconv.ParseInt: parsing "abcdefghi": invalid syntax
exit status 2

3.1 模块小结

log包的三类函数核心差异在于“是否终止程序”:Print仅记录、Fatal记录后退出、Panic记录后触发panic,需根据场景选择对应函数。

4 自定义标准logger输出格式

标准logger默认仅添加“日期+时间”字段,可通过SetFlags函数配置更多字段,灵活定制日志格式。

4.1 SetFlags函数的标志说明

SetFlags支持的核心标志及含义如下:

标志含义
Ldate本地时区的日期
Ltime本地时区的时间
Lmicroseconds微秒级别的时间
LUTC日期/时间使用UTC时区(而非本地时区)
Llongfile完整的文件名和行号
Lshortfile简化的文件名和行号(仅文件名+行号)
Lmsgprefix将前缀(SetPrefix设置)从行首移到消息开头前

4.2 标志使用示例

4.2.1 仅显示日期

log.SetFlags(log.Ldate) // 为每行日志前增加日期字段
log.Println("Some event happened")

输出结果

2022/01/24 Some event happened

4.2.2 日期+微秒级时间

// 用 | 运算符组合多个标志
log.SetFlags(log.Ldate | log.Lmicroseconds)
log.Println("Some event happened")

输出结果

2022/01/24 20:43:54.595365 Some event happened

4.2.3 日期+日志发生位置(文件名+行号)

log.SetFlags(log.Ldate | log.Lshortfile)
log.Println("Some event happened")

输出结果

2022/01/24 20:51:02 logging.go:20: Some event happened

4.2 模块小结

通过SetFlags函数可组合不同标志,定制日志包含的字段(日期、时间、文件行号等),提升日志的可追溯性。

5 日志输出目标配置

默认日志输出到标准错误(命令行),可通过SetOutput函数修改输出目标,如文件、同时输出到屏幕+文件。

5.1 输出到文件

5.1.1 操作步骤

  1. 以“创建+追加+只写”模式打开日志文件,权限设置为0644;
  2. 调用SetOutput将日志输出指向该文件;
  3. (可选)通过defer延迟关闭文件,避免资源泄漏。

5.1.2 示例代码

file, err := os.OpenFile("app.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
    log.Fatal(err)
}
defer file.Close()

log.SetOutput(file)
log.Println("Some event happened")

效果:日志不再输出到屏幕,仅写入app.log文件。

5.2 同时输出到屏幕与文件

5.2.1 核心原理

使用io.MultiWriter创建复合写入器,将输出流复制到多个目标(如os.Stderr(屏幕)+ 文件)。

5.2.2 示例代码

// 以追加、新建、只写的模式打开文件
file, err := os.OpenFile("app.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
    log.Fatal(err)
}
defer file.Close() // 程序退出时自动关闭文件

// 组合屏幕与文件写入器
writer := io.MultiWriter(os.Stderr, file)
log.SetOutput(writer) // 日志输出到复合写入器

log.Println("Some event happened")
log.Println("Another event happened")

效果:日志同时写入app.log文件和命令行窗口。

5.2 模块小结

SetOutput函数支持自定义日志输出目标,结合io.MultiWriter可实现多目标输出,满足不同环境的日志查看需求。

6 基于log包实现日志级别管理

日志级别可区分事件的严重性/优先级,便于过滤日志、减少警报疲劳。Go标准库log包未内置级别,但可通过New函数创建多logger实现级别划分。

6.1 日志级别定义

从高到低的核心日志级别:Fatal(致命)→ Error(错误)→ Warn(警告)→ Info(信息)→ Debug(调试)。

6.2 多logger构建级别体系

6.2.1 核心思路

为每个级别创建独立logger,通过SetPrefix为日志添加级别前缀,调用对应logger即可记录指定级别日志。

6.2.2 示例代码

var (
    info  *log.Logger // 用于记录 INFO 日志
    debug *log.Logger // 用于记录 DEBUG 日志
)

func init() {
    info = log.New(os.Stderr, "INFO\t", log.LstdFlags)  // 前缀为INFO
    debug = log.New(os.Stderr, "DEBUG\t", log.LstdFlags) // 前缀为DEBUG
}

func main() {
    info.Println("Some informational event happened")
    debug.Println("Some debugging event happened")
}

6.2.3 输出结果

INFO 2022/01/26 00:53:03 Some informational event happened
DEBUG 2022/01/26 00:53:03 Some debugging event happened

6.3 环境变量控制日志输出

6.3.1 核心思路

通过环境变量区分运行环境(开发/生产),仅在开发环境输出Debug日志,减少生产环境日志量。

6.3.2 环境变量设置

  • Unix/Linux/macOS:export ENV=production(生产)/export ENV=development(开发);
  • Windows:set ENV=production(生产)/set ENV=development(开发)。

6.3.3 示例代码

info.Println("Some informational event happened")
if os.Getenv("ENV") != "production" { // 仅非生产环境输出Debug
    debug.Println("Some debugging event happened")
}

6.4 日志级别过滤实操

基于Unix系统的grep命令可快速过滤指定级别日志,示例基于logfile.log(包含多级别日志):

需求命令效果
查看所有Error日志grep "^ERROR" logfile.log仅显示以ERROR开头的日志
排除Debug日志grep -v "^DEBUG" logfile.log显示除Debug外的所有日志

6.4 模块小结

通过多logger+前缀可实现日志级别划分,结合环境变量可控制不同环境的日志输出范围,grep等工具可快速过滤日志,提升排查效率。

7 接入系统syslog服务

syslog是基于网络的标准日志协议(RFC 3164/RFC 5424),适用于Linux系统(Windows不支持),Go可通过log/syslog包对接syslog(以Ubuntu 20.04的rsyslog为例)。

7.1 syslog消息结构

syslog消息包含三部分:

  1. Priority(优先级):由设施(facility,日志来源类型,如内核、用户进程)+ 严重性(severity,对应日志级别)组成;
  2. Header(头信息):时间戳 + 主机名/IP;
  3. Message(消息):标签(程序/进程名) + 内容(日志详情)。

7.1.1 核心设施(facility)

设施值含义
0kernel(内核)
1user-level(用户进程)
2mail(邮件系统)
3system daemon(系统守护进程)
4security/authorization(安全/授权)

7.1.2 严重性(severity)级别(0最高,7最低)

级别值名称对应日志级别
0Emergency紧急
1Alert告警
2Critical严重
3Error错误
4Warning警告
5Notice通知
6Informational信息
7Debug调试

7.2 rsyslog配置准备

7.2.1 操作步骤

  1. 编辑rsyslog配置文件:sudo vi /etc/rsyslog.conf

  2. $ActionFileDefaultTemplate RSYSLOG_TraditionalFileFormat后添加模板:

    $template gosyslogs,"%syslogseverity-text% %syslogfacility-text% %hostname% %timegenerated% %syslogtag% %msg%\n"
    $ActionFileDefaultTemplate gosyslogs
  3. 重启rsyslog服务:sudo service rsyslog restart

7.3 Go程序对接syslog

7.3.1 示例代码

package main

import (
    "log"
    "log/syslog"
)

var logger *log.Logger

func init() {
    var err error
    // 创建写入到 syslog 的 logger:设施为user-level,严重性为Notice
    logger, err = syslog.NewLogger(syslog.LOG_USER|syslog.LOG_NOTICE, 0)
    if err != nil {
        log.Fatal("cannot write to syslog: ", err)
    }
}

func main() {
    logger.Print("hello syslog!")
}

7.3.2 验证日志

运行程序:go run main.go; 查看syslog:sudo tail /var/log/syslog输出示例

notice user myhostname Jan 26 15:30:08 /tmp/go-build2223050573/b001/exe/main[163995]: hello syslog!

7.3 模块小结

log/syslog包可实现Go程序与syslog的对接,需先配置rsyslog模板,再通过NewLogger创建对应级别的logger,适用于Linux系统的日志集中管理场景。

本篇核心知识点速记

  1. log包核心函数:Print(仅记录)、Fatal(记录后退出)、Panic(记录后panic);
  2. 格式定制:SetFlags组合标志(日期、时间、文件行号等),SetPrefix添加日志前缀;
  3. 输出目标:SetOutput自定义输出(文件),MultiWriter实现多目标输出(屏幕+文件);
  4. 日志级别:New创建多logger+前缀实现级别划分,环境变量控制输出范围,grep过滤日志;
  5. syslog对接:Linux系统通过log/syslog包实现,需配置rsyslog模板,消息包含优先级、头信息、消息体三部分。