You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何让Go程序日志同时完整输出到终端与文件?

解决Go语言RunWithLog函数日志不同步问题

我编写了一个Go语言的RunWithLog函数,用于执行可执行文件并将其日志实时输出到终端,同时写入日志文件,但目前遇到问题:部分日志仅输出到终端,部分仅写入日志文件,无法实现完整同步输出。

原实现代码

RunWithLog函数

// Run an executable and print its log in real-time and print its log into a file too.
func RunWithLog(pthExe string, arg []string, fLog *os.File, dir string) error {
    cmd := exec.Command(pthExe, arg...)
    if dir != "" {
        cmd.Dir = dir
    }

    stdout, err := cmd.StdoutPipe()
    if err != nil {
        return err
    }
    stderr, err := cmd.StderrPipe()
    if err != nil {
        return err
    }

    var wg sync.WaitGroup
    wg.Add(4)

    go streamRealTime(stdout, &wg)
    go streamRealTime(stderr, &wg)

    go streamToLogFile(stdout, fLog, &wg)
    go streamToLogFile(stderr, &wg)

    err = cmd.Start()
    if err != nil {
        return err
    }

    wg.Wait()

    err = cmd.Wait()
    return err
}

func streamRealTime(output io.ReadCloser, wg *sync.WaitGroup) {
    defer wg.Done()
    scanner := bufio.NewScanner(output)
    scanner.Split(bufio.ScanRunes)
    for scanner.Scan() {
        fmt.Print(scanner.Text())
    }
    err := scanner.Err()
    if err != nil {
        log.Printf("error: write log to terminal: %s", err.Error())
    }
}

func streamToLogFile(output io.ReadCloser, fLog *os.File, wg *sync.WaitGroup) {
    defer wg.Done()
    scanner := bufio.NewScanner(output)
    scanner.Split(bufio.ScanRunes)
    for scanner.Scan() {
        _, err := fLog.WriteString(scanner.Text())
        if err != nil {
            log.Printf("error: write log to file: %s", err.Error())
        }
    }
    err := scanner.Err()
    if err != nil {
        log.Printf("error: write log to file: %s", err.Error())
    }
}

调用方式

// Log file.
fLog, err := os.OpenFile(pathLog, os.O_APPEND, os.ModeAppend)
if err != nil {
    return err
}
defer fLog.Close()

err = RunWithLog(pathExe, []string{"-b"}, fLog, workDir)
// ...

问题原因

核心问题是同一个stdout/stderr管道被两个goroutine同时读取。管道的流式数据是消费型的:第一个读取到数据的goroutine会把字节拿走,另一个goroutine就无法读取到这部分数据,最终导致日志被分流,部分内容只出现在终端,部分只写入文件。

解决方案

优化方案1:单goroutine读取后同时输出

让每个输出流(stdout/stderr)只由一个goroutine读取,读取到数据后同时输出到终端和写入文件,确保两份输出完全一致。

修改后的代码:

import (
    "bufio"
    "fmt"
    "io"
    "log"
    "os"
    "os/exec"
    "sync"
)

func streamBoth(output io.ReadCloser, fLog *os.File, wg *sync.WaitGroup) {
    defer wg.Done()
    scanner := bufio.NewScanner(output)
    scanner.Split(bufio.ScanRunes)
    for scanner.Scan() {
        text := scanner.Text()
        // 输出到终端
        fmt.Print(text)
        // 写入日志文件
        if _, err := fLog.WriteString(text); err != nil {
            log.Printf("error: write log to file: %s", err.Error())
        }
    }
    if err := scanner.Err(); err != nil {
        log.Printf("error: read output stream: %s", err.Error())
    }
}

// Run an executable and print its log in real-time and write its log into a file too.
func RunWithLog(pthExe string, arg []string, fLog *os.File, dir string) error {
    cmd := exec.Command(pthExe, arg...)
    if dir != "" {
        cmd.Dir = dir
    }

    stdout, err := cmd.StdoutPipe()
    if err != nil {
        return err
    }
    stderr, err := cmd.StderrPipe()
    if err != nil {
        return err
    }

    var wg sync.WaitGroup
    wg.Add(2)

    go streamBoth(stdout, fLog, &wg)
    go streamBoth(stderr, fLog, &wg)

    err = cmd.Start()
    if err != nil {
        return err
    }

    wg.Wait()

    return cmd.Wait()
}

优化方案2:使用io.MultiWriter简化实现

Go标准库的io.MultiWriter可以把多个Writer合并成一个,写入这个合并后的Writer时,数据会同时写入所有底层Writer。这种方式更简洁高效:

import (
    "io"
    "log"
    "os"
    "os/exec"
    "sync"
)

func streamToMulti(output io.ReadCloser, w io.Writer, wg *sync.WaitGroup) {
    defer wg.Done()
    // 用io.Copy把输出流复制到合并后的Writer
    if _, err := io.Copy(w, output); err != nil {
        log.Printf("error: copy output stream: %s", err.Error())
    }
}

// Run an executable and print its log in real-time and write its log into a file too.
func RunWithLog(pthExe string, arg []string, fLog *os.File, dir string) error {
    cmd := exec.Command(pthExe, arg...)
    if dir != "" {
        cmd.Dir = dir
    }

    stdout, err := cmd.StdoutPipe()
    if err != nil {
        return err
    }
    stderr, err := cmd.StderrPipe()
    if err != nil {
        return err
    }

    // 创建同时写入终端和日志文件的Writer
    multiWriter := io.MultiWriter(os.Stdout, fLog)

    var wg sync.WaitGroup
    wg.Add(2)

    go streamToMulti(stdout, multiWriter, &wg)
    go streamToMulti(stderr, multiWriter, &wg)

    err = cmd.Start()
    if err != nil {
        return err
    }

    wg.Wait()

    return cmd.Wait()
}

调用代码优化

原调用代码中打开文件时仅指定os.O_APPEND,如果文件不存在会报错,建议补充创建权限:

// Log file.
fLog, err := os.OpenFile(pathLog, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
    return err
}
defer fLog.Close()

err = RunWithLog(pathExe, []string{"-b"}, fLog, workDir)
// ...

内容的提问来源于stack exchange,提问作者Megidd

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.11 01:59:52