引言

提到如何停止 GO 程序,或许我们的第一印象就是我一个 Ctrl+C 不就解决了么。 还有什么需要聊的?哈哈哈~ 回归正题,这里我们聊的是在程序中,程序自身的停止,而非收到外力强行停止程序。 在 Golang 中,有两个常用的方法 os.Exit()log.Fatal() 来实现终止程序。

比如程序发版的时候,我们需要终止程序,然后运行新的程序,那么则可以监听一个 kill 信号,然后程序收到信号之后,等待协程处理完成,释放资源,最后 os.Exit(0)

os.Exit() 和 log.Fatal() 的差异

这两个函数的差异可以分别从使用场景、输出的信息、资源回收、返回值这几方面来对比

使用场景

os.Exit() 用于程序需要立即停止的情况,它通常在程序的最后阶段调用,比如在处理完所有必要的清理工作后退出程序。

log.Fatal() 用于记录一个致命错误信息,并且立即退出程序。它通常用于无法恢复的错误,如配置错误、文件系统错误等。

输出信息

os.Exit() 只负责退出程序,不会输出任何错误信息。

log.Fatal() 会先将错误信息输出到标准错误输出(stderr),然后调用 os.Exit(1) 来退出程序。

资源回收

os.Exit() 允许你在退出前执行一些清理操作,比如关闭文件句柄、释放资源等。

log.Fatal() 会在退出前执行一些默认的日志记录器的清理操作,比如同步日志文件。

返回值

os.Exit() 允许你指定一个整数作为程序的退出码,这个退出码会被操作系统接收,用于表示程序的退出状态。

log.Fatal() 默认使用退出码 1,表示程序因为严重错误而终止。

os.Exit() 和 log.Fatal() 底层实现

os.Exit()

函数在标准款文件:os/proc.go#L62

func Exit(code int) {
	if code == 0 && testlog.PanicOnExit0() {
		panic("unexpected call to os.Exit(0) during test")
	}
	
	//开始释放资源
	runtime_beforeExit(code)

   //执行系统回调,退出指令
	syscall.Exit(code)
}

然后我们跟进 runtime_beforeExit 函数,进入到 runtime 包,文件见:runtime/proc.go#L305os_beforeExit 里面做了几件事:

  1. runExitHooks 执行一个在程序退出时执行的函数,如果有对个的则遵循FIFO的原则来执行执行
  2. 这个钩子是什么呢?比如我们经常使用关闭文件的 defer fs.Close() ,就会在这里面执行
  3. 判断是否是正常退出,当 extiCode 是 0 的时候表示正常退出,是正常退出,则检查 raceenabled 是否存在数据竞争检测,这个全局变量是在执行go build 和go run 指定命令选项 -race, 则为 true。
  4. 更多关于 raceenabled 这个可以官方的文章:race-detector
// os_beforeExit is called from os.Exit(0).
//
//go:linkname os_beforeExit os.runtime_beforeExit
func os_beforeExit(exitCode int) {
	runExitHooks(exitCode)
	if exitCode == 0 && raceenabled {
		racefini()
	}
}

log.Fatal()

函数在标准款文件:log/log.go#L282

func (l *Logger) Fatal(v ...any) {
	l.Output(2, fmt.Sprint(v...))
	os.Exit(1)
}

函数首先会将错误信息输出到标准错误输出。这是通过 log.Output 方法实现的,它通常会将信息写入到 os.Stderr。 然后,log.Fatal() 会调用 os.Exit(1) 来退出程序。 这里的 1 是一个约定俗成的退出码,表示程序遇到了无法恢复的错误。

总结

总的来说,os.Exit() 和 log.Fatal() 都可以导致程序的退出。
但 log.Fatal() 会输出错误信息,而 os.Exit() 不会。 在实际编程中,我们可以根据具体的错误情况和需要来选择合适的函数。 如果需要记录错误信息并立即停止程序,使用 log.Fatal() 是最好不过的; 如果只是需要退出程序,并且已经通过其他方式记录了错误信息,那么使用 os.Exit() 可能更合适。