软件帮帮网
柔彩主题三 · 更轻盈的阅读体验

Go并发日志记录方法:高效处理多协程日志输出

发布时间:2026-01-07 22:50:29 阅读:53 次

在开发高并发服务时,日志记录是排查问题、监控系统状态的重要手段。Go语言凭借其轻量级的goroutine和channel机制,在并发编程上表现出色。但当多个协程同时写日志时,如果处理不当,很容易出现日志错乱、文件锁竞争甚至性能下降的问题。

为什么不能直接用fmt写日志?

有些开发者图省事,直接用fmt.Fprintf往文件里写日志。但在并发场景下,多个goroutine同时写同一个文件,会导致日志内容交错。比如用户A的请求日志和用户B的日志混在一起,查问题时根本分不清谁是谁。

使用标准库log搭配互斥锁

最简单的解决方案是加锁。Go标准库log支持自定义输出目标,配合sync.Mutex就能保证线程安全。

package main

import (
    "log"
    "os"
    "sync"
)

var (
    logger = log.New(os.Stdout, "", log.LstdFlags|log.Lshortfile)
    mu     sync.Mutex
)

func writeLog(msg string) {
    mu.Lock()
    defer mu.Unlock()
    logger.Println(msg)
}

这种方式简单可靠,适合中小型项目。但锁会带来性能开销,特别是在高频写入场景下,所有goroutine都要排队等锁,反而成了瓶颈。

用channel实现异步日志

更优雅的做法是把日志写入变成异步操作。启动一个专门的“日志协程”,其他协程通过channel发送日志消息。

package main

import (
    "bufio"
    "log"
    "os"
)

var logChan = make(chan string, 1000)

func init() {
    file, _ := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
    writer := bufio.NewWriter(file)
    
    go func() {
        for msg := range logChan {
            writer.WriteString(msg + "\n")
            writer.Flush() // 实际项目中可考虑批量刷新
        }
    }()
}

func logAsync(msg string) {
    select {
    case logChan <- msg:
    default:
        // 防止channel满时阻塞
    }
}

这种模式下,业务协程只需把日志发到channel就立刻返回,真正写磁盘由后台协程完成。即使磁盘IO慢,也不会拖垮主流程。

推荐使用成熟日志库zap

对于线上服务,建议直接使用uber开源的zap库。它专为高性能设计,支持结构化日志,并内置了并发安全的写入机制。

package main

import (
    "go.uber.org/zap"
)

func main() {
    logger, _ := zap.NewProduction()
    defer logger.Sync()
    
    // 多个goroutine可安全调用
    for i := 0; i < 10; i++ {
        go func(id int) {
            logger.Info("处理请求", zap.Int("user_id", id))
        }(i)
    }
}

zap的性能远超标准库,格式也更规范。像字节跳动、腾讯这些大厂的Go服务都在用它记日志。

实际场景中的小技巧

假设你在做一个抢红包系统,每秒有上万次请求。每个请求都要记录“用户X领取了Y元”。如果每个请求都同步写文件,磁盘很快就会成为瓶颈。这时候用异步channel或zap这类高性能库,就能轻松扛住压力。

另外记得给日志加时间戳和唯一请求ID。不然几十万行日志堆在一起,根本没法定位具体某次请求的完整链路。