Go by Example - Spawning Processes, Execing Processes, Signals and Exit

Go by Example - Spawning Processes, Execing Processes, Signals and Exit

介绍go中的生成进程, 执行进程, 信号和退出

Spawning Processes

有时,我们的Go程序需要派生其他非Go进程。

spawning-processes.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
package main

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

func main() {
// 我们将从一条简单的命令开始,这条命令不需要参数或输入,只需将内容打印到 stdout
// exec.Command 辅助程序会创建一个对象来表示这个外部进程
dateCmd := exec.Command("date")

// Output 方法运行命令,等待命令完成并收集其标准输出
// 如果没有错误,dateOut 将保留包含日期信息的字节
dateOut, err := dateCmd.Output()
if err != nil {
panic(err)
}
fmt.Println("> date")
fmt.Println(string(dateOut))

// 如果执行命令时出现问题(如路径错误),Command 的 Output 和其他方法将返回 *exec.Error;如果命令运行但退出时返回代码非零,则返回 *exec.ExitError
_, err = exec.Command("date", "-x").Output()
if err != nil {
switch e := err.(type) {
case *exec.Error:
fmt.Println("failed executing:", err)
case *exec.ExitError:
fmt.Println("command exit rc =", e.ExitCode())
default:
panic(err)
}
}

// 接下来,我们将研究一种稍微复杂的情况,即通过管道将数据传送到外部进程的 stdin,并从其 stdout 中收集结果
grepCmd := exec.Command("grep", "hello")

// 在这里,我们明确地抓取输入/输出管道,启动进程,向其写入一些输入,读取结果输出,最后等待进程退出
grepIn, _ := grepCmd.StdinPipe()
grepOut, _ := grepCmd.StdoutPipe()
grepCmd.Start()
grepIn.Write([]byte("hello grep\ngoodbye grep"))
grepIn.Close()
grepBytes, _ := io.ReadAll(grepOut)
grepCmd.Wait()

// 在上述示例中,我们省略了错误检查,但你可以使用常见的 if err != nil 模式对所有错误进行检查
// 我们还只收集了 StdoutPipe 的结果,但你也可以用完全相同的方法收集 StderrPipe 的结果
fmt.Println("> grep hello")
fmt.Println(string(grepBytes))

// 请注意,在生成命令时,我们需要提供一个明确划分的命令和参数数组,而不能只传递一个命令行字符串
// 如果你想用字符串生成完整的命令,可以使用 bash 的 -c 选项
lsCmd := exec.Command("bash", "-c", "ls -a -l -h")
lsOut, err := lsCmd.Output()
if err != nil {
panic(err)
}
fmt.Println("> ls -a -l -h")
fmt.Println(string(lsOut))
}

生成的程序返回的输出结果与我们直接从命令行运行它们的结果相同。date 没有 -x 标志,因此它会以错误信息和非零返回代码退出。

log
1
2
3
4
5
6
7
8
9
10
11
12
$ go run spawning-processes.go 
> date
Thu 05 May 2022 10:10:12 PM PDT

command exited with rc = 1
> grep hello
hello grep

> ls -a -l -h
drwxr-xr-x 4 mark 136B Oct 3 16:29 .
drwxr-xr-x 91 mark 3.0K Oct 3 12:50 ..
-rw-r--r-- 1 mark 1.3K Oct 3 16:28 spawning-processes.go

Exec'ing Processes

在上一个示例中,我们了解了如何生成外部进程。当我们需要一个外部进程可以访问正在运行的 Go 进程时,我们就会这样做。有时,我们只是想用另一个进程(可能是非 Go 进程)完全替换当前的 Go 进程。为此,我们将使用 Go 实现的经典 exec 函数。

execing-processes.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

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

func main() {
// 在我们的例子中,我们将执行 ls
// Go 需要我们要执行的二进制文件的绝对路径,因此我们将使用 exec.LookPath 来查找它(可能是 /bin/ls)
binary, lookErr := exec.LookPath("ls")
if lookErr != nil {
panic(lookErr)
}

// Exec 要求参数为片段形式(而不是一个大字符串)
// 我们将给 ls 提供几个常用参数。请注意,第一个参数应该是程序名称
args := []string{"ls", "-a", "-l", "-h"}

// Exec 还需要使用一组环境变量(https://gobyexample.com/environment-variables)。这里我们只提供当前环境
env := os.Environ()

// 下面是实际的 syscall.Exec 调用
// 如果调用成功,我们的进程将在此结束,并由 /bin/ls -a -l -h 进程取代
// 如果出现错误,我们将得到一个返回值
execErr := syscall.Exec(binary, args, env)
if execErr != nil {
panic(execErr)
}
}

当我们运行程序时,它将被 ls 取代。

log
1
2
3
4
5
$ go run execing-processes.go
total 16
drwxr-xr-x 4 mark 136B Oct 3 16:29 .
drwxr-xr-x 91 mark 3.0K Oct 3 12:50 ..
-rw-r--r-- 1 mark 1.3K Oct 3 16:28 execing-processes.go

请注意,Go 并不提供经典的 Unix fork 函数。不过这通常不是问题,因为启动 goroutines、生成进程和执行进程涵盖了 fork 的大多数使用情况。

Signals

有时,我们希望 Go 程序能智能地处理 Unix 信号。例如,我们可能希望服务器在收到 SIGTERM 时优雅地关闭,或者命令行工具在收到 SIGINT 时停止处理输入。下面介绍如何在 Go 中使用通道处理信号。

signals.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main

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

func main() {
// Go 信号通知通过在通道上发送 os.Signal 值来实现
// 我们将创建一个通道来接收这些通知。请注意,该通道应该是缓冲的
sigs := make(chan os.Signal, 1)

// signal.Notify 注册指定通道以接收指定信号的通知
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)

// 我们可以在主函数中接收来自 sigs 的信息,但让我们来看看如何在一个单独的程序中实现这一功能,以演示更真实的优雅关机场景
done := make(chan bool, 1)

// 该程序执行阻塞接收信号。一旦收到信号,它就会打印出来,然后通知程序可以结束
go func() {

sig := <-sigs
fmt.Println()
fmt.Println(sig)
done <- true
}()

// 程序将在此等待,直到收到预期信号(如上面的 goroutine 在完成时发送值所示),然后退出
fmt.Println("awaiting signal")
<-done
fmt.Println("exiting")
}
log
1
2
3
4
5
$ go run signals.go
awaiting signal
^C
interrupt
exiting

当我们运行这个程序时,它会阻塞等待信号。通过键入 ctrl-C(终端显示为 ^C),我们可以发送 SIGINT 信号,使程序打印中断,然后退出。

Exit

使用 os.Exit 可以立即以给定的状态退出。

exit.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import (
"fmt"
"os"
)

func main() {
// 在使用 os.Exit 时,defers 将不会运行,因此该 fmt.Println 将永远不会被调用
defer fmt.Println("!")

// 以状态 3 退出
os.Exit(3)
}

请注意,与 C 不同,Go 不使用 main 的整数返回值来指示退出状态。如果要以非零状态退出,则应使用 os.Exit。
如果使用 go run 运行 exit.go,退出将被 go 提取并打印出来。
通过构建和执行二进制文件,你可以在终端中看到状态。

log
1
2
3
4
5
6
7
$ go run exit.go
exit status 3

$ go build exit.go
$ ./exit
$ echo $?
3

请注意,我们程序中的 ! 从未打印出来。

参考链接

Go by Example - Spawning Processes, Execing Processes, Signals and Exit

https://blog.wty.cool/2024/06/09/go_by_example/Spawning_Processes-Execing_Processes-Signals-Exit/

作者

孤独小狼

发布于

2024-06-09

更新于

2024-06-09

许可协议

评论