如何让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
相关产品推荐
相关产品推荐

