파이프
Source: Dev.to

The problem
Go에서 다음과 같은 셸 명령과 동일하게 동작하는 파이프라인을 실행하고 싶었습니다:
tail -f test.log | head -3
tail -f는 스스로 종료되지 않으므로, 첫 번째 명령이 계속 실행되는 동안 두 번째(head)가 필요한 줄 수를 읽고 종료되는 상황을 파이프라인이 처리해야 합니다.
Demonstration in the shell
# In one terminal
echo line1 > test.log
tail -f ./test.log | head -3
다른 터미널에서:
echo line2 >> ./test.log
head는 줄이 나타나는 즉시 출력합니다. 세 번째 줄이 기록되면 head는 세 번째 줄을 받고 종료합니다. 네 번째 쓰기에서는 파이프의 읽기 끝이 닫혔기 때문에 tail이 SIGPIPE를 받아 종료됩니다.
Bash 문서에서도 이 동작을 확인할 수 있습니다:
파이프라인의 각 명령 출력은 다음 명령 입력에 파이프를 통해 연결됩니다. 각 명령은 이전 명령의 출력을 읽습니다.
다중 명령 파이프라인의 각 명령은 자체 서브쉘에서 실행되며, 이는 별도의 프로세스입니다.
— Bash Reference Manual – Pipelines
Mimicking the behavior in Go
Creating the commands
cmd1 := exec.Command("tail", "-f", "./test.log")
cmd2 := exec.Command("head", "-n", "5")
Connecting the pipeline
outPipe, err := cmd1.StdoutPipe()
if err != nil {
// handle error
}
cmd2.Stdin = outPipe
cmd2.Stdout = os.Stdout // forward head's output to the Go program's stdout
Starting the processes
if err := cmd1.Start(); err != nil {
// handle error
}
if err := cmd2.Start(); err != nil {
// handle error
}
Waiting for completion
cmd2(즉 head 명령)가 먼저 끝납니다. head가 종료된 후 부모 프로세스에서 파이프의 읽기 끝을 닫아야 합니다; 그렇지 않으면 파이프가 열려 있게 되고 tail은 SIGPIPE를 받지 못합니다.
cmd2.Wait() // wait for head to finish
outPipe.Close() // close the pipe in the parent
cmd1.Wait() // now tail can exit
Complete example
package main
import (
"fmt"
"os"
"os/exec"
)
func main() {
cmd1 := exec.Command("tail", "-f", "/Users/skuf_love/study/go/test_pipes/test.log")
cmd2 := exec.Command("head", "-n", "5")
outPipe, err := cmd1.StdoutPipe()
if err != nil {
fmt.Printf("StdoutPipe error: %v\n", err)
return
}
cmd2.Stdin = outPipe
cmd2.Stdout = os.Stdout
if err = cmd1.Start(); err != nil {
fmt.Printf("cmd1 start error: %v\n", err)
return
}
if err = cmd2.Start(); err != nil {
fmt.Printf("cmd2 start error: %v\n", err)
return
}
// Wait for head to finish, then close the pipe so tail receives SIGPIPE
cmd2.Wait()
fmt.Println("cmd2 wait done")
outPipe.Close()
cmd1.Wait()
fmt.Println("cmd1 wait done")
}
이 프로그램은 셸 파이프라인 동작을 재현합니다: head는 test.log에 기록된 처음 다섯 줄을 읽고 종료하며, tail은 SIGPIPE를 받아 종료됩니다.