Golang实现后台进程运行及日志记录指南

Golang实现后台进程运行及日志记录指南 我想以后台进程的方式运行一个Perl脚本,并将其输出重定向到一个文件。

这是一个 print 脚本的示例:

#!/usr/bin/perl

$i = 10; while($i--) {print time()."\n"; sleep 5;}

以下是我运行它的方式:

package main

import (
	"fmt"
	"os"
)

func main() {
	file, err := os.OpenFile("/tmp/out.log", os.O_RDWR|os.O_CREATE, 0755)
	if err != nil {
		fmt.Println("failed to open log file")
		return
	}

	attr := os.ProcAttr{
		Env: os.Environ(),
		Files: []*os.File{
			nil,
			file,
			file,
		},
	}
	process, err := os.StartProcess("/tmp/print", []string{"/tmp/print"}, &attr)
	if err != nil {
		fmt.Println("failed to start process: %v", err)
		return
	}

	err = process.Release()
	if err != nil {
		fmt.Println("failed to release: %v", err)
	}
}

这个方法可以运行,但是 out.log 文件在脚本执行完成之前不会被写入内容。如果我使用 os.Stdout 而不是文件,那么输出会立即逐行打印出来。

由于这个脚本预计会运行很长时间,我希望能在日志文件中看到实际的输出。有什么方法可以实现这一点吗?


更多关于Golang实现后台进程运行及日志记录指南的实战教程也可以访问 https://www.itying.com/category-94-b0.html

4 回复

你说得对。 我也用刷新标准输出的bash和python脚本测试过,代码按预期工作。 非常感谢!

更多关于Golang实现后台进程运行及日志记录指南的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


在Perl中,开启自动刷新:$| = 1 参见 perlvar - Perl 预定义变量 - Perldoc 浏览器

与Go代码无关,看起来没问题。实际问题是你的Perl脚本。我不是这门语言的专家,但我搜索了一些细节,我的猜测是,print 实际上不会立即将数据写入文件。由于你一直保持文件打开,数据会等待,直到资源被释放,以便以最少的系统调用次数将所有行写入文件。在这种情况下,你可以手动添加缓冲区 flush,这样你的打印内容就会实时添加到文件中。或者,每次你想写入内容时,重新打开文件进行追加。你可以查看这篇文章,并尝试将其添加到你的Perl代码中。

在Go中实现后台进程并实时记录日志,需要正确处理文件缓冲和进程输出流。以下是改进后的实现方案:

package main

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

func main() {
    // 创建日志文件(使用追加模式)
    file, err := os.OpenFile("/tmp/out.log", 
        os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
    if err != nil {
        fmt.Printf("failed to open log file: %v\n", err)
        return
    }
    defer file.Close()

    // 创建带缓冲的写入器
    writer := bufio.NewWriter(file)
    defer writer.Flush()

    // 创建命令
    cmd := exec.Command("/tmp/print")
    
    // 获取标准输出管道
    stdout, err := cmd.StdoutPipe()
    if err != nil {
        fmt.Printf("failed to get stdout pipe: %v\n", err)
        return
    }

    // 获取标准错误管道
    stderr, err := cmd.StderrPipe()
    if err != nil {
        fmt.Printf("failed to get stderr pipe: %v\n", err)
        return
    }

    // 启动进程
    if err := cmd.Start(); err != nil {
        fmt.Printf("failed to start process: %v\n", err)
        return
    }

    // 使用WaitGroup等待goroutine完成
    var wg sync.WaitGroup
    wg.Add(2)

    // 实时读取并写入标准输出
    go func() {
        defer wg.Done()
        scanner := bufio.NewScanner(stdout)
        for scanner.Scan() {
            line := scanner.Text()
            writer.WriteString(line + "\n")
            writer.Flush() // 立即刷新缓冲区
            fmt.Printf("STDOUT: %s\n", line) // 可选:控制台输出
        }
    }()

    // 实时读取并写入标准错误
    go func() {
        defer wg.Done()
        scanner := bufio.NewScanner(stderr)
        for scanner.Scan() {
            line := scanner.Text()
            writer.WriteString("ERROR: " + line + "\n")
            writer.Flush() // 立即刷新缓冲区
            fmt.Printf("STDERR: %s\n", line) // 可选:控制台输出
        }
    }()

    // 等待进程完成
    if err := cmd.Wait(); err != nil {
        fmt.Printf("process finished with error: %v\n", err)
    }

    // 等待所有goroutine完成
    wg.Wait()
    fmt.Println("Process completed, logs saved to /tmp/out.log")
}

如果需要完全后台运行(脱离终端),可以使用以下方法:

package main

import (
    "fmt"
    "os"
    "os/exec"
    "syscall"
)

func daemonize() {
    // 创建守护进程
    cmd := exec.Command("/tmp/print")
    
    // 分离进程组
    cmd.SysProcAttr = &syscall.SysProcAttr{
        Setsid: true,
    }
    
    // 重定向输出到文件
    logFile, _ := os.OpenFile("/tmp/out.log", 
        os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
    defer logFile.Close()
    
    cmd.Stdout = logFile
    cmd.Stderr = logFile
    
    // 启动并释放
    if err := cmd.Start(); err != nil {
        fmt.Printf("failed to start daemon: %v\n", err)
        return
    }
    
    fmt.Printf("Daemon started with PID: %d\n", cmd.Process.Pid)
    
    // 立即退出父进程,子进程继续在后台运行
    os.Exit(0)
}

func main() {
    daemonize()
}

对于长时间运行的进程,建议添加日志轮转功能:

package main

import (
    "fmt"
    "io"
    "os"
    "os/exec"
    "path/filepath"
    "time"
)

type RotatingWriter struct {
    currentFile *os.File
    filePath    string
    maxSize     int64
    currentSize int64
}

func NewRotatingWriter(path string, maxSize int64) (*RotatingWriter, error) {
    f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
    if err != nil {
        return nil, err
    }
    
    stat, _ := f.Stat()
    return &RotatingWriter{
        currentFile: f,
        filePath:    path,
        maxSize:     maxSize,
        currentSize: stat.Size(),
    }, nil
}

func (rw *RotatingWriter) Write(p []byte) (n int, err error) {
    if rw.currentSize+int64(len(p)) > rw.maxSize {
        rw.rotate()
    }
    
    n, err = rw.currentFile.Write(p)
    rw.currentSize += int64(n)
    return
}

func (rw *RotatingWriter) rotate() {
    rw.currentFile.Close()
    
    // 重命名旧日志文件
    timestamp := time.Now().Format("20060102_150405")
    backupPath := rw.filePath + "." + timestamp
    os.Rename(rw.filePath, backupPath)
    
    // 创建新日志文件
    f, _ := os.OpenFile(rw.filePath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
    rw.currentFile = f
    rw.currentSize = 0
}

func main() {
    writer, _ := NewRotatingWriter("/tmp/out.log", 10*1024*1024) // 10MB轮转
    
    cmd := exec.Command("/tmp/print")
    stdout, _ := cmd.StdoutPipe()
    stderr, _ := cmd.StderrPipe()
    
    cmd.Start()
    
    // 同时复制stdout和stderr
    go io.Copy(writer, stdout)
    go io.Copy(writer, stderr)
    
    cmd.Wait()
    writer.currentFile.Close()
}

关键点:

  1. 使用exec.Command替代os.StartProcess以获得更好的控制
  2. 通过管道实时读取进程输出
  3. 使用bufio.Writer并手动调用Flush()确保立即写入
  4. 对于守护进程,使用Setsid: true分离进程组
  5. 考虑日志轮转避免文件过大
回到顶部