如何为Go子进程的stdout输出添加前缀?
问题
是否可以为子进程的stdout添加前缀?已知stdout属于io.Writer类型且不可变。我曾考虑为每个子进程添加logrus钩子来在日志中显示PID,但仍想了解是否有修改stdout的方法。
以下测试代码启动3个子进程,将它们的stdout和stderr重定向到主程序的stdout和stderr:
package main import ( "os" "os/exec" "testing" "time" "github.com/sirupsen/logrus" ) func Crasher(t *testing.T) { logrus.Info("huhaha") // This runs test with lots of logs. } func startProcess(pid int) { cmd := exec.Command(os.Args[0], "-test.run=^TestCrasher$") cmd.Env = append(os.Environ(), "BE_CRASHER=1") cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr logrus.Infof("Starting process with pid %d", pid) _ = cmd.Run() } func TestCrasher(t *testing.T) { if os.Getenv("BE_CRASHER") == "1" { Crasher(t) return } for pid := 0; pid < 3; pid++ { go startProcess(pid) } time.Sleep(5 * time.Second) }
当前输出:
=== RUN TestCrasher time="2009-11-10T23:00:00Z" level=info msg="Starting process with pid 0" time="2009-11-10T23:00:00Z" level=info msg="Starting process with pid 2" time="2009-11-10T23:00:00Z" level=info msg="Starting process with pid 1" time="2009-11-10T23:00:00Z" level=info msg=huhaha time="2009-11-10T23:00:00Z" level=info msg=huhaha time="2009-11-10T23:00:00Z" level=info msg=huhaha PASS PASS PASS --- PASS: TestCrasher (5.00s) PASS
期望输出:
time="2009-11-10T23:00:00Z" level=info msg="Starting process with pid 0" time="2009-11-10T23:00:00Z" level=info msg="Starting process with pid 2" time="2009-11-10T23:00:00Z" level=info msg="Starting process with pid 1" pid0 time="2009-11-10T23:00:00Z" level=info msg=huhaha pid1 time="2009-11-10T23:00:00Z" level=info msg=huhaha pid2 time="2009-11-10T23:00:00Z" level=info msg=huhaha
解决方案
可以通过实现自定义io.Writer给子进程输出添加前缀,无需修改原始stdout(确实不可变),只需将自定义Writer作为子进程的Stdout/Stderr,写入时自动添加前缀。
实现步骤
- 定义带前缀的Writer结构体,包含目标Writer和前缀字符串
- 实现
Write方法,按行分割输入内容,给每行添加前缀后写入目标Writer - 启动子进程时,将cmd.Stdout和cmd.Stderr替换为自定义Writer
修改后的代码
package main import ( "bufio" "io" "os" "os/exec" "testing" "time" "github.com/sirupsen/logrus" ) // PrefixWriter 给输出添加前缀的自定义Writer type PrefixWriter struct { prefix string writer io.Writer } func NewPrefixWriter(prefix string, w io.Writer) *PrefixWriter { return &PrefixWriter{ prefix: prefix, writer: w, } } // Write 实现io.Writer接口,处理字节流并添加前缀 func (pw *PrefixWriter) Write(data []byte) (n int, err error) { scanner := bufio.NewScanner(io.NewReader(data)) totalWritten := 0 for scanner.Scan() { line := scanner.Text() output := []byte(pw.prefix + " " + line + "\n") written, writeErr := pw.writer.Write(output) if writeErr != nil { return totalWritten, writeErr } totalWritten += written } return len(data), scanner.Err() } func Crasher(t *testing.T) { logrus.Info("huhaha") // This runs test with lots of logs. } func startProcess(pid int) { cmd := exec.Command(os.Args[0], "-test.run=^TestCrasher$") cmd.Env = append(os.Environ(), "BE_CRASHER=1") // 使用自定义PrefixWriter包装os.Stdout和os.Stderr prefix := "pid" + string(rune(pid+'0')) cmd.Stdout = NewPrefixWriter(prefix, os.Stdout) cmd.Stderr = NewPrefixWriter(prefix, os.Stderr) logrus.Infof("Starting process with pid %d", pid) _ = cmd.Run() } func TestCrasher(t *testing.T) { if os.Getenv("BE_CRASHER") == "1" { Crasher(t) return } for pid := 0; pid < 3; pid++ { go startProcess(pid) } time.Sleep(5 * time.Second) }
说明
- 自定义
PrefixWriter会按行处理子进程输出,每行前自动添加指定前缀,再写入原始stdout - 无需修改子进程代码,完全在主进程层面处理输出,避免了给每个子进程配置logrus钩子的复杂度
- 输出会符合期望格式,每个子进程的日志行前都会带上对应的pid标识
内容的提问来源于stack exchange,提问作者subtleseeker
相关产品推荐
相关产品推荐

