Over the course of learning Go, I’ve come to like some aspects of the language. 1 of those is the defer statement. For those that aren’t familiar with the language, defer queues up a function call for just before your current function exits. I’ve found it similar in purpose to a finally statement in Java, a keyword that helps ensure that some piece of code is run. It’s just that defer can be put right next to the thing I want to clean up, instead of trying to remember to include it at the very end of a wrapper around everything. As part of testing what I write by just running the code, I’m setting up a databases for test packages, and using defer to clean them up once the package tests are done. Except…the defer wasn’t running. So, what gives?
The set-up
So, to set up my tests, I was doing something like this:
func TestMain(m *testing.M) {
copied, err := test.SetupTestDatabase(srcDB, dbPath)
if err != nil {
log.Fatal("Could not create test database ", dbPath, ": ", err)
}
// Yes, I slog over multiple lines, get over it.
logger.InfoContext(
ctx,
"Created test database",
slog.String("filename", dbPath),
slog.Int64("size", copied),
)
defer func () error {
err := test.CleanupTestDatabase(dbPath)
if err != nil {
log.Fatal("Error cleaning the database!", err)
}
} ()
/*
Get a connection to the database
Start a test server
*/
// I read somewhere this was a best practice, so I'm trying it.
exitCode := m.Run()
os.Exit(exitCode)
}
On the surface, this makes sense…except the database wasn’t getting cleaned up by the code. That’s a problem, because that’s the whole point of calling defer.
The Hypotheses
Looking at this code block, I had 2 hypotheses. The first, os.Exit(exitCode) exits in a manner that bypasses defer statements. That seemed like the most obvious, but why would something skip the cleanup method, rather than just call it early? Go also has panic (which is similar to failing an assert in other languages), and that calls defers. So maybe there’s something else I’m missing. In this case, my other hypothesis was that the function I was wrapping around my cleanup logic was done wrong, The problem with this hypothesis, was that (as you may have noticed), it’s not a particularly complicated setup. Time for a quick experiment.
The Test
So…I wrote a simple little Go script to simulate the different exit conditions:
package main
import (
"fmt"
"os"
"strconv"
"strings"
)
func main() {
args := os.Args
/*
Set up a background context
Set up logger for the test server
*/
wrapDefer, err := strconv.ParseBool(args[2])
if err != nil {
panic("invalid defer flag, must use a boolean")
}
switch strings.ToLower(args[1]) {
case "panic":
deferPanicWrapper(wrapDefer)
case "exit":
deferOSWrapper(wrapDefer)
default:
defaultWrapper(wrapDefer)
}
}
func callOSExit() {
os.Exit(0)
}
func callPanic() {
panic("Paniced")
}
func defaultWrapper(wrapped bool) error {
if wrapped {
defer func() {
err := deferredAction()
if err != nil {
panic(fmt.Errorf("deferred error: %v", err))
}
}()
} else {
defer deferredAction()
}
return nil
}
func deferredAction() error {
fmt.Println("Deferred action")
return fmt.Errorf("deferred action error")
}
func deferOSWrapper(wrapped bool) error {
if wrapped {
defer func() {
err := deferredAction()
if err != nil {
panic(fmt.Errorf("deferred error: %v", err))
}
}()
} else {
defer deferredAction()
}
callOSExit()
return nil
}
func deferPanicWrapper(wrapped bool) error {
if wrapped {
defer func() {
err := deferredAction()
if err != nil {
panic(fmt.Errorf("deferred error: %v", err))
}
}()
} else {
defer deferredAction()
}
callPanic()
return nil
}
Now I have a script I can call via go run main.go {panic | exit} {true | false} to trigger the error condition and defer version combination that I want to test. Running these combinations gave me these results:

Looking at the results, I ran all the tests, but only the panic triggered output. Interesting, mostly be because if I had to bet which of panic and os.Exit(statusCode) doesn’t call defer, I’d have argued that panic had a stronger case to ignore defer.
While I was skeptical that os.Exit(statusCode)was the problem (just because surely the language wouldn’t create a “cleanup before you leave this method” statement and didn’t just write an exit scenario that ignored it, right?
Well, yeah. It appears os.Exit(statusCode) is a hard exit. Do not pass Go (pun absolutely intended), do not call deferred statements on the way out. Cool (and yes, I could have searched the os package documentation and probably have seen this there, but I’m trying to learn Go and typing out a script to try these things out is just more practice.
So, how did I end up solving my own problem? By pulling the cleanup out of the defer and just calling the cleanup exactly when I want it:
func TestMain(m *testing.M) {
copied, err := test.SetupTestDatabase(srcDB, dbPath)
if err != nil {
log.Fatal("Could not create test database ", dbPath, ": ", err)
}
// Yes, I slog over multiple lines, get over it.
logger.InfoContext(
ctx,
"Created test database",
slog.String("filename", dbPath),
slog.Int64("size", copied),
)
/*
Get a connection to the database
Start a test server
*/
// I read somewhere this was a best practice, so I'm trying it.
exitCode := m.Run()
err := test.CleanupTestDatabase(dbPath)
if err != nil {
log.Fatal("Error cleaning the database!", err)
}
os.Exit(exitCode)
}
Basically the same thing, but with manual placement for the cleanup rather than the handy defer. The other option was removing the OS-level status code and exit, but I do like the idea of running the tests and a failing test triggering a “failed” status code when running the tests as a script.
To date, nothing beats answering simple questions with a quick test script (OK, you can beat it for speed, but not for getting things to stick into your head). os.Exit(statusCode)doesn’t call defer before stopping the code. panic does. My test databases are cleaning up, and now I know why I have to manually call that logic at the end of the setup. And if you use os.Exit(statusCode) in your TestMain(m *testing.M) function and found yourself unable to defer, now you know why too.