Skip to content

Go 语言 log 包使用教程

Go 语言的 log 包提供了简单而高效的日志记录功能,是开发调试和生产监控的重要工具。

1. 基础概念

在 Go 的 log 模块中,核心概念包括:

  • log.Logger: 日志记录器,负责格式化和输出日志信息。
  • 日志级别: 包括普通日志、致命错误(Fatal)和恐慌(Panic)三种级别。
  • 日志格式: 支持自定义前缀、时间戳格式和输出标志。
  • 输出目标: 可以将日志输出到标准输出、标准错误、文件或任何实现了 io.Writer 接口的对象。

为什么使用 log 包?

虽然可以直接使用 fmt.Println 输出调试信息,但 log 包提供了更多优势:

  • 时间戳: 自动添加时间戳,便于追踪问题发生时间
  • 格式统一: 提供一致的日志格式
  • 输出控制: 可以轻松切换输出目标
  • 错误处理: 提供 Fatal 和 Panic 级别的错误处理
  • 线程安全: 多个 goroutine 可以安全地并发写入日志

2. 快速入门

基本日志输出

最简单的日志使用方式是直接调用 log 包的函数:

go
package main

import (
    "log"
)

func main() {
    // 输出普通日志信息
    log.Print("这是一条日志")
    log.Println("这是一条带换行的日志")
    log.Printf("这是格式化日志:用户ID=%d, 用户名=%s", 1001, "Alice")
}

运行结果:

shell
2025/11/05 14:30:15 这是一条日志2025/11/05 14:30:15 这是一条带换行的日志
2025/11/05 14:30:15 这是格式化日志:用户ID=1001, 用户名=Alice

不同级别的日志

log 包提供了三个级别的日志函数:

go
package main

import (
    "log"
)
func main() {
    log.Println("这是一条很普通的日志。")
    v := "很普通的"
    log.Printf("这是一条%s日志。\n", v)
    // Fatalln 会触发 fatal 错误,程序将退出
    log.Fatalln("这是一条会触发fatal的日志。")
    // Panicln 会触发 panic 错误,程序将退出
    log.Panicln("这是一条会触发panic的日志。")
    // 下面的代码不会执行
    fmt.Println("下面的代码不会执行")
}

运行结果:

shell
2025/11/05 14:04:17 这是一条很普通的日志。
2025/11/05 14:04:17 这是一条很普通的日志。
2025/11/05 14:04:17 这是一条会触发fatal的日志。
exit status 1

3. 自定义日志格式

日志标志说明

log标准库提供了如下的flag选项,它们是一系列定义好的常量。

go
const (
	Ldate         = 1 << iota     // 日期:2009/01/23
	Ltime                         // 时间:01:23:23
	Lmicroseconds                 // 微秒级别时间:01:23:23.123123. 等价于:Ltime | Lmicroseconds
	Llongfile                     // 文件全路径名+行号: /a/b/c/d.go:23
	Lshortfile                    // 文件名+行号:d.go:23. 等价于:Lfile | Lline
	LUTC                          // 使用UTC时间
	LstdFlags     = Ldate | Ltime // 标准日期时间格式:2009/01/23 01:23:23
)

设置日志前缀和标志

可以通过 SetPrefixSetFlags 自定义日志格式:

go
package main

import (
    "log"
)

func main() {
    // 设置日志前缀
    log.SetPrefix("[MyApp] ")
    
    // 设置日志标志
    log.SetFlags(log.Ldate | log.Ltime | log.Lmicroseconds | log.Lshortfile)
    
    log.Println("这是自定义格式的日志")
    log.Printf("当前用户:%s", "Bob")
}

运行结果:

shell
[MyApp] 2025/11/05 14:30:15.123456 main.go:12: 这是自定义格式的日志
[MyApp] 2025/11/05 14:30:15.123789 main.go:13: 当前用户:Bob

4. 自定义日志器

创建新的日志器

使用 log.New 可以创建自定义的日志器:

go
package main

import (
    "log"
    "os"
)

func main() {
    // 创建不同用途的日志器
    
    // 信息日志器
    infoLogger := log.New(os.Stdout, "INFO: ", log.Ldate|log.Ltime|log.Lshortfile)
    
    // 警告日志器
    warnLogger := log.New(os.Stdout, "WARN: ", log.Ldate|log.Ltime|log.Lshortfile)
    
    // 错误日志器 - 输出到标准错误
    errorLogger := log.New(os.Stderr, "ERROR: ", log.Ldate|log.Ltime|log.Lshortfile)
    
    // 使用不同的日志器
    infoLogger.Println("应用程序启动")
    warnLogger.Println("配置文件未找到,使用默认配置")
    errorLogger.Println("数据库连接失败")
}

运行结果:

shell
INFO: 2025/11/05 14:30:15 main.go:16: 应用程序启动
WARN: 2025/11/05 14:30:15 main.go:17: 配置文件未找到,使用默认配置
ERROR: 2025/11/05 14:30:15 main.go:18: 数据库连接失败

日志器结构体封装

创建一个结构化的日志器来管理不同级别的日志:

go
package main

import (
    "io"
    "log"
    "os"
)

// Logger 封装不同级别的日志器
type Logger struct {
    info  *log.Logger
    warn  *log.Logger
    error *log.Logger
}

// NewLogger 创建新的日志器
func NewLogger(infoWriter, warnWriter, errorWriter io.Writer) *Logger {
    return &Logger{
        info:  log.New(infoWriter, "INFO: ", log.Ldate|log.Ltime|log.Lshortfile),
        warn:  log.New(warnWriter, "WARN: ", log.Ldate|log.Ltime|log.Lshortfile),
        error: log.New(errorWriter, "ERROR: ", log.Ldate|log.Ltime|log.Lshortfile),
    }
}

// Info 记录信息日志
func (l *Logger) Info(v ...interface{}) {
    l.info.Println(v...)
}

// Warn 记录警告日志
func (l *Logger) Warn(v ...interface{}) {
    l.warn.Println(v...)
}

// Error 记录错误日志
func (l *Logger) Error(v ...interface{}) {
    l.error.Println(v...)
}

func main() {
    // 创建自定义日志器
    logger := NewLogger(os.Stdout, os.Stdout, os.Stderr)
    
    // 使用日志器
    logger.Info("用户登录成功", "userID=1001")
    logger.Warn("API 调用频率过高")
    logger.Error("数据库查询失败")
}

5. 日志输出控制

输出到文件

将日志输出到文件是生产环境的常见需求:

go
package main

import (
    "log"
    "os"
)

func main() {
    // 创建或打开日志文件
    logFile, err := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
    if err != nil {
        log.Fatal("无法打开日志文件:", err)
    }
    defer logFile.Close()
    
    // 设置日志输出到文件
    log.SetOutput(logFile)
    log.SetPrefix("[App] ")
    log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
    
    // 记录日志
    log.Println("应用程序启动")
    log.Printf("当前工作目录: %s", getCurrentDir())
    log.Println("初始化完成")
}

func getCurrentDir() string {
    dir, _ := os.Getwd()
    return dir
}

同时输出到多个目标

使用 io.MultiWriter 可以同时输出到多个目标:

go
package main

import (
    "io"
    "log"
    "os"
)

func main() {
    // 创建日志文件
    logFile, err := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
    if err != nil {
        log.Fatal("无法创建日志文件:", err)
    }
    defer logFile.Close()
    
    // 创建多重写入器 - 同时写入文件和标准输出
    multiWriter := io.MultiWriter(os.Stdout, logFile)
    
    // 创建日志器
    logger := log.New(multiWriter, "[App] ", log.Ldate|log.Ltime|log.Lshortfile)
    
    // 记录日志 - 会同时输出到控制台和文件
    logger.Println("应用程序启动")
    logger.Println("配置加载完成")
    logger.Println("服务器开始监听端口 8080")
}

6. 实际应用示例

构建一个 Web 服务器日志系统

go
package main

import (
    "fmt"
    "io"
    "log"
    "net/http"
    "os"
    "time"
)

// WebLogger Web 服务器日志器
type WebLogger struct {
    access *log.Logger
    error  *log.Logger
}

// NewWebLogger 创建 Web 日志器
func NewWebLogger() (*WebLogger, error) {
    // 创建访问日志文件
    accessFile, err := os.OpenFile("access.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
    if err != nil {
        return nil, fmt.Errorf("无法创建访问日志文件: %v", err)
    }
    
    // 创建错误日志文件
    errorFile, err := os.OpenFile("error.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
    if err != nil {
        accessFile.Close()
        return nil, fmt.Errorf("无法创建错误日志文件: %v", err)
    }
    
    return &WebLogger{
        access: log.New(io.MultiWriter(os.Stdout, accessFile), "[ACCESS] ", log.Ldate|log.Ltime),
        error:  log.New(io.MultiWriter(os.Stderr, errorFile), "[ERROR] ", log.Ldate|log.Ltime|log.Lshortfile),
    }, nil
}

// LogAccess 记录访问日志
func (wl *WebLogger) LogAccess(r *http.Request, status int, duration time.Duration) {
    wl.access.Printf("%s %s %s %d %v", 
        r.RemoteAddr, r.Method, r.URL.Path, status, duration)
}

// LogError 记录错误日志
func (wl *WebLogger) LogError(err error, context string) {
    wl.error.Printf("%s: %v", context, err)
}

// LoggingMiddleware 日志中间件
func LoggingMiddleware(logger *WebLogger, next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        
        // 包装 ResponseWriter 以捕获状态码
        wrapped := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
        
        // 调用下一个处理器
        next.ServeHTTP(wrapped, r)
        
        // 记录访问日志
        duration := time.Since(start)
        logger.LogAccess(r, wrapped.statusCode, duration)
    })
}

// responseWriter 包装器,用于捕获状态码
type responseWriter struct {
    http.ResponseWriter
    statusCode int
}

func (rw *responseWriter) WriteHeader(code int) {
    rw.statusCode = code
    rw.ResponseWriter.WriteHeader(code)
}

// 处理器函数
func homeHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "欢迎访问首页!")
}

func apiHandler(logger *WebLogger) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        // 模拟可能的错误
        if r.URL.Query().Get("error") == "true" {
            logger.LogError(fmt.Errorf("模拟的API错误"), "API处理")
            http.Error(w, "内部服务器错误", http.StatusInternalServerError)
            return
        }
        
        fmt.Fprintf(w, `{"message": "API 调用成功", "timestamp": "%s"}`, time.Now().Format(time.RFC3339))
    }
}

func main() {
    // 创建日志器
    logger, err := NewWebLogger()
    if err != nil {
        log.Fatal("初始化日志器失败:", err)
    }
    
    // 设置路由
    mux := http.NewServeMux()
    mux.HandleFunc("/", homeHandler)
    mux.HandleFunc("/api", apiHandler(logger))
    
    // 应用日志中间件
    handler := LoggingMiddleware(logger, mux)
    
    // 启动服务器
    logger.access.Println("服务器启动在端口 :8080")
    if err := http.ListenAndServe(":8080", handler); err != nil {
        logger.LogError(err, "服务器启动")
    }
}

使用示例:

shell
# 启动服务器
$ go run main.go

# 在另一个终端测试
$ curl http://localhost:8080/
$ curl http://localhost:8080/api
$ curl http://localhost:8080/api?error=true

7. 最佳实践

1. 日志级别管理

  • 在开发环境使用 DEBUG 级别,生产环境使用 INFO 或更高级别
  • 合理使用 Fatal 和 Panic,避免在库代码中使用
  • 为不同模块创建独立的日志器

2. 日志格式规范

  • 保持日志格式的一致性
  • 包含必要的上下文信息(时间戳、文件位置等)
  • 使用结构化的日志前缀

3. 性能考虑

  • 避免在高频调用的代码中使用过多日志
  • 考虑使用条件日志来减少不必要的输出
  • 对于大量日志,考虑异步写入

8. 局限性与替代方案

log 包的局限性

  1. 功能简单: 只提供基础的日志功能,缺少日志级别控制
  2. 性能一般: 对于高并发场景,性能可能不够理想
  3. 格式固定: 日志格式相对固定,自定义能力有限
  4. 缺少结构化: 不支持结构化日志(JSON 格式等)

推荐的替代方案

对于复杂的日志需求,推荐使用第三方日志库:

  • logrus: 功能丰富,支持结构化日志和多种输出格式
  • zap: 高性能日志库,适合高并发场景
  • zerolog: 零分配的 JSON 日志库
  • slog: Go 1.21+ 新增的结构化日志包

9. 总结

Go 的 log 包是一个简单实用的日志记录工具,适合大多数基本场景。它的优势在于:

  • 标准库: 无需额外依赖
  • 简单易用: API 设计直观
  • 线程安全: 支持并发使用
  • 灵活输出: 可以输出到任何 io.Writer

虽然功能相对基础,但对于简单到中等复杂度的应用来说,log 包完全够用。当需要更高级功能(如日志级别、结构化日志、高性能等)时,可以考虑使用第三方库。