3.1.管道

\(3.1.\)管道

1.系统命令创建

  • 创建命令:
1
cmd0 := exec.Command("echo", "-n", "My first command comes from golang. ")
  • 启动命令:
1
2
3
if err := cmd0.Start(); err != nil {
...
}

2.读取数据

   StdoutPipe()方法返回的对象类型为io.ReadCloser,这是拓展了io.Reader接口的接口类型。我们可以调用它的Read方法获取命令输出:

1
2
3
4
5
6
7
8
// 用于存放数据的字节切片
output0 := make([]byte, 30)
n, err := stdout0.Read(output0)
if err := nil {
...
}

fmt.Printf("%s\n", output0[:n])

  如果输出管道没有可以读取的数据,Read方法返回的err就会是io.EOF

  如果切片的长度过小,可以通过for循环迭代读取:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var outputBuf0 bytes.Buffer
for {
tempOutput := make([]byte, 5)
n, err := stdout0.Read(tempOutput)
if err != nil {
if err == io.EOF {
break
} else {
...
}
}

if n > 0 {
outputBuf0.Write(tempOutput[:n])
}
}

  还可以使用带缓冲的读取器来读取输出管道中的数据,这样更加简便:

1
2
3
4
5
6
7
outputBuf0 := bufio.NewReader(stdout0)
output0, _, err := outputBuf0.ReadLine()
if err != nil {
...
}

fmt.Printf("%s\n", string(output0))

  由于stdout0也具有io.Reader接口类型,它可以作为bufio.NewReader的参数。

3.创建管道

\(a.\)匿名管道

  • 创建获取此命令的管道:
1
2
3
4
stdout0, err := cmd0.StdoutPipe()
if err != nil {
...
}

  管道可以把一个命令的输出作为另一个命令的输入,在go中的例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
cmd1 := exec.Command("ps", "aux")
cmd2 := exec.Command("grep", "apipe")

var outputBuf1 bytes.Buffer

cmd1.Stdout = &outputBuf1
if err := cmd1.Start(); err != nil {
...
}

if err := cmd1.wait(); err != nil {
...
}

cmd2.Stdin = &outputBuf1
var outputBuf2 bytes.Buffer
cmd2.Stdout = &outputBuf2
if err := cmd2.Start(); err != nil {
...
}

由于bytes.Buffer实现了io.Writerio.Reader接口,因此可以将它赋值给.Stdout.Stdin

cmd1.wait会一直阻塞,直到cmd1完全运行结束。

  • 匿名管道会在管道缓冲区被写满之后使写数据的进程阻塞

\(b.\)命名管道

  与匿名管道不同,任何进程都可以通过命名管道交换数据

实际上,命名管道以文件的形式存在于文件系统中,使用它的方法与使用文件很类似。

  go中创建这样的管道的API如下:

1
reader, writer, err := os.Pipe()

  这里返回的readerwriter分别为管道输入端、输出端的*os.File类型值。并发运行下面的代码,就可以让reader读取writer写入的数据了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
go func() {
n, err := writer.Write(input)
if err != nil {
...
}
}()

go func() {
output := make([]byte, 100)
n, err := reader.Read(output)
if err != nil {
...
}
}()

  为什么要并发运行呢?因为管道的两端(读取端和写入端)在某一端未准备就绪时会阻塞

  • 如果写入端试图写入管道,但管道的缓冲区已满,则写入操作会阻塞,直到读取端读取了足够的数据。

  • 如果读取端试图从管道读取,但管道中没有数据,则读取操作会阻塞,直到写入端写入了数据。

  如果顺序执行的话,程序就会一直被阻塞了。

  命名管道可以被多路复用,这使得我们需要考虑操作原子性。go的io包里提供了一个保证原子性的管道:

1
reader, writer := io.Pipe()

  readerwriter分别为*io.PipeReader*io.PipeWriter类型变量。