go基础库之从子进程读写

每个执行进程都具有标准输出、输入和错误输出。Go标准库提供了对这些进行读写的方法。

从子进程读写

Golang 版本

1.12.1

前言

每个执行进程都具有标准输出、输入和错误输出。Go标准库提供了对这些进行读写的方法。

实现

  1. 创建文件main_read_output.go,代码如下:
package main

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

func main() {
	var cmd string
	if runtime.GOOS == "windows" {
		cmd = "dir"
	} else {
		cmd = "ls"
	}

	proc := exec.Command(cmd)

	// 进程终止并返回标准输出
	buff, err := proc.Output()

	if err != nil {
		panic(err)
	}
	// 子进程的输出以字节切片的形式存在打印为字符串
	fmt.Println(string(buff))
}

$ go run main_read_output.go
main_read_output.go
  1. 创建文件main_read_stdout.go,代码如下:
package main

import (
"bytes"
"fmt"
"os/exec"
"runtime"
)

func main() {

	var cmd string

	if runtime.GOOS == "windows" {
		cmd = "dir"
	} else {
		cmd = "ls"
	}

	proc := exec.Command(cmd)

	buf := bytes.NewBuffer([]byte{})

	// 实现io.Writer接口的缓冲区被分配给进程的Stdout
	proc.Stdout = buf

	// 在这个例子中避免竞争条件。我们等到进程退出
	proc.Run()

	// 该过程将输出写入缓冲区,我们使用字节来打印输出
	fmt.Println(string(buf.Bytes()))

}
$ go run main_read_stdout.go
main_read_output.go
main_read_stdout.go
  1. 创建文件main_read_read.go,代码如下:
package main

import (
	"bufio"
	"context"
	"fmt"
	"os/exec"
	"time"
)

func main() {
	cmd := "ping"
	timeout := 2 * time.Second

	// 命令行工具“ping”执行2秒
	ctx, _ := context.WithTimeout(context.TODO(), timeout)
	proc := exec.CommandContext(ctx, cmd, "example.com")

	// 进程输出以io.ReadCloser的形式获得。底层实现使用os.Pipe
	stdout, _ := proc.StdoutPipe()
	defer stdout.Close()

	proc.Start()

	// 为了更舒适地阅读,使用了bufio.Scanner。
	s := bufio.NewScanner(stdout)
	for s.Scan() {
		fmt.Println(s.Text())
	}
}
$ go run main_read_read.go
PING example.com (93.184.216.34) 56(84) bytes of data.
64 bytes from 93.184.216.34 (93.184.216.34): icmp_seq=1 ttl=52 time=211 ms
64 bytes from 93.184.216.34 (93.184.216.34): icmp_seq=2 ttl=52 time=257 ms
  1. 创建文件sample.go,代码如下:

    package main
    
    import (
    "bufio"
    "fmt"
    "os"
    )
    
    func main() {
    	sc := bufio.NewScanner(os.Stdin)
    
    	for sc.Scan() {
    		fmt.Println(sc.Text())
    	}
    }
    

    创建文件main.go,代码如下:

    package main
    
    import (
    	"bufio"
    	"fmt"
    	"io"
    	"os/exec"
    	"time"
    )
    
    func main() {
    	cmd := []string{"go", "run", "sample.go"}
    
    	proc := exec.Command(cmd[0], cmd[1], cmd[2])
    
    	stdin, _ := proc.StdinPipe()
    	defer stdin.Close()
    
    	// 出于调试目的,我们会监视已执行进程的输出
    	stdout, _ := proc.StdoutPipe()
    	defer stdout.Close()
    
    	go func() {
    		s := bufio.NewScanner(stdout)
    		for s.Scan() {
    			fmt.Println("程序输出:" + s.Text())
    		}
    	}()
    
    	proc.Start()
    
    	// 现在,以下行被写入子进程标准输入
    	fmt.Println("写入")
    	io.WriteString(stdin, "Hello\n")
    	io.WriteString(stdin, "Golang\n")
    	io.WriteString(stdin, "is awesome\n")
    
    	time.Sleep(time.Second * 2)
    
    	proc.Process.Kill()
    
    }
    
    $ go run main.go
    写入
    程序输出:Hello
    程序输出:Golang
    程序输出:is awesome
    

原理

os/exec 包中的Cmd结构体提供了访问进程输入/输出的功能。有几种方法可以读取进程的输出。

读取进程输出的最简单方法之一是使用Cmd结构体的OutputCombinedOutput(获取StderrStdout)。在调用此方法时,程序同步地等待子进程终止,然后将输出返回到字节缓冲区。

除了OutputOutputCombined方法之外,Cmd结构体还提供了Stdout属性,可以为其分配io.Writer。然后,分配的写入器作为进程输出的目标。它可以是文件、字节缓冲区或实现io.Writer接口的任何类型。

读取进程输出的最后一种方法是通过调用StdoutPipe方法从Cmd结构体中获取io.ReaderStdoutPipe方法在Stdout之间创建管道,进程写入输出,并提供Reader作为程序接口以读取进程输出。 这样,进程的输出通过管道传输到取回的io.Reader

写入进程stdin的工作方式相同。 在所有选项中,将演示使用io.Writer的选项。

可以看出,有几种方法可以从子进程读取和写入。 stderrstdin的使用几乎与步骤2中描述的相同。 最后,如何访问输入/输出的方法可以这样划分:

  • 同步(等待进程结束并获取字节):使用CmdOutputCombinedOutput方法。
  • IO:输出或输入以io.Writer/Reader的形式提供。 XXXPipeStdXXX属性是此方法的正确属性。

IO类型更灵活,也可以异步使用。