Go’s defer is one of those things you quickly fall in love with. It’s perfect for cleaning up and neatly ensuring steps are always run when exiting some code block.
Many devs coming in from other languages might be used to calling some equivalent of os.Exit
(e.g: sys.exit
in python) or think the log
package’s log.Fatalln
is a handy way of “error and quit” but here’s the catch: if you call os.Exit
or log.Fatalln
, all those lovely deferred functions you’re relying on? Yeah, they don’t get called.
This might surprise many, especially given that panic
does allow the defer stack to unwind.
Let’s dive straight into some code.
package main
import (
"fmt"
"log"
"os"
)
func main() {
defer fmt.Println("This will NOT be printed")
// Uncomment one of these to see the problem:
// os.Exit(1)
// log.Fatalln("Goodbye!")
}
While the example seems benign, in some cases you might have:
package main
import (
"some.remote.repo.io/remoteLoggingPackage"
)
func main() {
logger := remoteLoggingPackage.New()
defer logger.Close()
if err := run(); err != nil {
logger.Errorf("error: ", err)
}
}
If someone calls os.Exit
or log.Fatalln
somewhere inside run
, or run returns an error, there’s a good chance the last few log calls will never get sent to the remote.
I’ve seen this in production where error logs start disappearing, and surprisingly the defer->debug.SetTraceback->debug.Stack block isn’t running only to see there’s no panic but instead someone snuck in an os.Exit
call thinking it’d work similar to panic.
Keep a tiny main, have it call another function (e.g: run
), do all your defers in there and call os.Exit
in main.
package main
import (
"os"
"some.remote.repo.io/remoteLoggingPackage"
)
func main() {
os.Exit(run())
}
// no point returning errors, only error codes
func run() int {
logger := remoteLoggingPackage.New()
defer logger.Close()
// set up your defers
// start code execution
// on errors / end of execution return appropriate exit code
}
Always let errors bubble up, it’s the way to go with Go.