In this tutorial, we will guide you on how to handle defer calls with the os.Exit(). The defer keyword in Go is a statement used to defer the execution of a function until the surrounding function returns. In other words, the defer statement postpones the execution of a function or statement until the end of the function calling the function. The defer statement is invoked when the surrounding function executes a return statement, reaches the end of its function body, or because the surrounding goroutine is panicking.
The execution of the defer functions is not always guaranteed
As mentioned above, the defer function will be invoked when the main function return. However, the deferred functions are not always guaranteed to be executed. For example, invoking os.Exit
or pressing CTRL + C
to interrupt the program will result in your deferred functions not being called.
func Exit(code int)
: Exit causes the current program to exit with the given status code. Conventionally, code zero indicates success, non-zero an error. The program terminates immediately; deferred functions are not run.
Let’s take a look at the example below to see how it occurs:
package main
import (
"os"
"time"
)
func main() {
defer func() {
fmt.Println("This is a defer function")
}()
time.Sleep(5 * time.Second)
os.Exit(1)
}
Output:
exit status 1
Method-1: Handle defer calls with the os.Exit
If you want to make sure the defer function will be called even if you use the os.Exit()
function, you can re-arrange your code as the code shown below. By using a function that returns a code and has a defer function inside it, we can make sure the defer function is eventually invoked even when we can the os.Exit()
function.
package main
import (
"fmt"
"os"
)
func deferFunc() int {
// defer function
defer func() {
fmt.Println("This is the defer function!")
}()
// return a random code
return 1
}
func main() {
fmt.Println("This is the main function!")
os.Exit(deferFunc())
}
Output:Â
This is the main function!
This is the defer function!
exit status 1
Method-2: Wrap the os.Exit inside a defer function
We can wrap the os.Exit()
function inside a defer function to make sure other defer calls will be invoked. The example below has 3 defer functions, one of which wraps the os.Exit()
function:
package main
import (
"fmt"
"os"
)
func main() {
exitCode := 0
defer func() { os.Exit(exitCode) }()
defer func() {
fmt.Println("This is defer function no 1")
}()
defer func() {
fmt.Println("This is defer function no 2")
}()
testNum := 1
for testNum < 100 {
fmt.Println("Test number:", testNum)
if testNum > 3 {
// update the exitCode
exitCode = 1
return
}
testNum += 1
}
}
Output:
Test number: 1
Test number: 2
Test number: 3
Test number: 4
This is defer function no 2
This is defer function no 1
exit status 1
Method-3: Handle defer calls with the os.Exit and the Goexit()
func Goexit()
: 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 a panic, any recover calls in those deferred functions will return nil.
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. Because we call Goexit() from the main goroutine, at the top of main
you need to call defer os.Exit(0)
:
The example below how to use the Goexit()
to handle defer calls with the os.Exit()
:
package main
import (
"fmt"
"os"
"runtime"
"time"
)
func main() {
defer os.Exit(1)
defer func() {
fmt.Println("This is the defer function")
}()
fmt.Println("This is the main function")
time.Sleep(3 * time.Second)
runtime.Goexit()
}
Output:
Method-4: Handle defer calls with the os.Exit and panic, recover
We can use panic
and recover
to handle the defer calls in this case. The panic
will invoke the defer functions but will also always exit with a non-0 status code and dump a stack trace. We can override the behavior of the panic as the code shown below:
package main
import (
"fmt"
"os"
)
type Exit struct{ Code int }
// exit handler
func exitHandler() {
fmt.Println("This is the defer function")
if e := recover(); e != nil {
if exit, ok := e.(Exit); ok == true {
os.Exit(exit.Code)
}
panic(e) // not an Exit
}
}
func main() {
defer exitHandler() // the handler
defer fmt.Println("This is the main function")
panic(Exit{2})
}
Output:
This is the main function
This is the defer function
exit status 2
Summary
The first example shows us that the deferred functions are not always guaranteed to be executed (in the case of using os.Exit()
function). To handle the defer calls with os.Exit
, we can use a function that returns a code and has a defer function inside it, or wrap the os.Exit
inside a defer function. Another way to ensure the defer function will be invoked is by using the runtime.Goexit()
function. The last method to take control of the defer calls with os.Exit
is by panic and recovery keywords.
References
https://pkg.go.dev/os#Exit
https://pkg.go.dev/runtime#Goexit
How to exit a go program honoring deferred calls?