How to exit a go program honoring deferred calls?

huangapple go评论83阅读模式
英文:

How to exit a go program honoring deferred calls?

问题

我需要使用defer来手动释放使用C库创建的内存分配,但我也需要在某个时刻使用非零状态的os.Exit。棘手的部分是os.Exit会跳过任何已延迟的指令:

package main

import "fmt"
import "os"

func main() {

    // 使用`os.Exit`时,`defer`不会被执行,所以这个`fmt.Println`永远不会被调用。
    defer fmt.Println("!")
    // 有时候人们会使用`defer`来执行关键操作,比如关闭数据库、删除锁或释放内存。

    // 使用指定的状态码退出程序。
    os.Exit(3)
}

Playground: http://play.golang.org/p/CDiAh9SXRM,源自https://gobyexample.com/exit

那么如何在Go程序中退出并执行已声明的defer调用呢?是否有os.Exit的替代方案?

英文:

I need to use defer to free allocations manually created using C library, but I also need to os.Exit with non 0 status at some point. The tricky part is that os.Exit skips any deferred instruction:

package main

import "fmt"
import "os"

func main() {

    // `defer`s will _not_ be run when using `os.Exit`, so
    // this `fmt.Println` will never be called.
    defer fmt.Println("!")
    // sometimes ones might use defer to do critical operations
    // like close a database, remove a lock or free memory

    // Exit with status code.
    os.Exit(3)
}

Playground: http://play.golang.org/p/CDiAh9SXRM stolen from https://gobyexample.com/exit

So how to exit a go program honoring declared defer calls? Is there any alternative to os.Exit?

答案1

得分: 57

只需将程序下移一级并返回退出代码:

package main

import "fmt"
import "os"

func doTheStuff() int {
    defer fmt.Println("!")
    
    return 3
}

func main() {
    os.Exit(doTheStuff())
}
英文:

Just move your program down a level and return your exit code:

package main

import "fmt"
import "os"

func doTheStuff() int {
	defer fmt.Println("!")

	return 3
}

func main() {
	os.Exit(doTheStuff())
}

答案2

得分: 40

runtime.Goexit() 是实现这一目标的简单方法。

> Goexit 终止调用它的 goroutine。不会影响其他 goroutine。**Goexit 在终止 goroutine 之前运行所有延迟调用。**然而,由于 Goexit 不是 panic,因此在这些延迟函数中的任何 recover 调用都将返回 nil。

然而:

> 从主 goroutine 调用 Goexit 会终止该 goroutine,而不会使 func main 返回。由于 func main 尚未返回,程序会继续执行其他 goroutine。如果所有其他 goroutine 退出,程序将崩溃。

因此,如果你从主 goroutine 调用它,在 main 函数的顶部你需要添加

defer os.Exit(0)

在此之下,你可能还想添加一些其他的 defer 语句,用于通知其他 goroutine 停止并进行清理。

英文:

runtime.Goexit() is the easy way to accomplish that.

> Goexit terminates the goroutine that calls it. No other goroutine is affected. Goexit runs all deferred calls before terminating the goroutine. Because Goexit is not panic, however, any recover calls in those deferred functions will return nil.

However:

> Calling Goexit from the main goroutine terminates that goroutine without func main returning. Since func main has not returned, the program continues execution of other goroutines. If all other goroutines exit, the program crashes.

So if you call it from the main goroutine, at the top of main you need to add

defer os.Exit(0)

Below that you might want to add some other defer statements that inform the other goroutines to stop and clean up.

答案3

得分: 34

经过一些研究,参考了这个链接,我找到了一个替代方案:

  • 不像https://stackoverflow.com/a/27629493/438563那样强制使用特定的架构
  • 不像https://stackoverflow.com/a/24601700/438563那样需要任何全局值

我们可以利用panicrecover。事实证明,panic本质上会遵守defer调用,但总是以非0状态码退出并转储堆栈跟踪。关键在于,我们可以通过以下方式覆盖panic行为的最后一个方面:

package main

import "fmt"
import "os"

type Exit struct{ Code int }

// 退出码处理程序
func handleExit() {
    if e := recover(); e != nil {
        if exit, ok := e.(Exit); ok == true {
            os.Exit(exit.Code)
        }
        panic(e) // 不是Exit类型,向上冒泡
    }
}

现在,要在任何位置退出程序并仍保留任何声明的defer指令,我们只需要触发一个Exit类型:

func main() {
    defer handleExit() // 插入退出处理程序
    defer fmt.Println("cleaning...")
    panic(Exit{3}) // 3是退出码
}

除了在func main内插入一行代码外,它不需要任何重构:

func main() {
    defer handleExit()
    // 准备就绪
}

这在处理较大的代码库时非常有效,所以我将其提供给您进行审查。希望对您有所帮助。

英文:

After some research, refer to this this, I found an alternative that:

We can take advantage of panic and recover. It turns out that panic, by nature, will honor defer calls but will also always exit with non 0 status code and dump a stack trace. The trick is that we can override last aspect of panic behavior with:

package main

import "fmt"
import "os"

type Exit struct{ Code int }

// exit code handler
func handleExit() {
	if e := recover(); e != nil {
		if exit, ok := e.(Exit); ok == true {
			os.Exit(exit.Code)
		}
		panic(e) // not an Exit, bubble up
	}
}

Now, to exit a program at any point and still preserve any declared defer instruction we just need to emit an Exit type:

func main() {
	defer handleExit() // plug the exit handler
	defer fmt.Println("cleaning...")
	panic(Exit{3}) // 3 is the exit code
}

It doesn't require any refactoring apart from plugging a line inside func main:

func main() {
    defer handleExit()
    // ready to go
}

This scales pretty well with larger code bases so I'll leave it available for scrutinization. Hope it helps.

Playground: http://play.golang.org/p/4tyWwhcX0-

答案4

得分: 29

对于后代来说,对我来说,这是一个更优雅的解决方案:

func main() { 
    retcode := 0
    defer func() { os.Exit(retcode) }()
    defer defer1()
    defer defer2()

    [...]

    if err != nil {
        retcode = 1
        return
    }
}
英文:

For posterity, for me this was a more elegant solution:

func main() { 
	retcode := 0
	defer func() { os.Exit(retcode) }()
	defer defer1()
	defer defer2()

	[...]

	if err != nil {
    	retcode = 1
        return
	}
}

huangapple
  • 本文由 发表于 2014年12月24日 07:14:53
  • 转载请务必保留本文链接:https://go.coder-hub.com/27629380.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定