The Go programming language provides a defer keyword, which registers a statement to be executed after the current function returns. This is useful for a variety of purposes, including situations where you are performing a task that involves multiple setup steps, and you want to gracefully tear down what you set up. The notion of a “destructor” in object-oriented programming does something which, at a high enough level of abstraction, can be thought of as similar (tear-down of setup steps required in order to create some object and/or perform some task).

	MyType.CONSTRUCTOR()
		SetupStep1()
		SetupStep2()
		SetupStep3()
		SetupStep4()

	MyType.DoStuff()
		...

	MyType.DESTRUCTOR()
		TeardownStep4()
		TeardownStep3()
		TeardownStep2()
		TeardownStep1()

But the patterns available with Go’s defer have important advantages over this kind of approach.

Consider: what happens if there is a failure of some kind halfway through your setup steps? (E.g. SetupSteps 1 and 2 have completed successfully, but SetupStep3() fails with an error.) With a “destructor” strategy, the code in the destructor would have to inspect exactly how many / which of the setup steps did or did not succeed - or consult some data structure in which this information is maintained - and thus, which setup steps do and don’t require tear-down. This is certainly doable; but it requires separate bookkeeping of the progress of individual setup steps.1

And even if such bookkeeping were in place, the result would still be code that is less legible than it’s defer-based counterpart - if only because, as we will see, the defer-based pattern allows you to write each tear-down step right next to its corresponding setup step. Whereas the constructor/destructor pattern seen here requires a second kind of bookkeeping - this time in the programmer’s head - to ensure that for every setup step in the constructor, a corresponding tear-down step has indeed been added to the destructor.


With defer, Go allows you to push statements onto a stack that will then be executed on a last-in first-out basis, as soon as the current function returns. Importantly, these steps can be pushed incrementally - in our case, pushing a new tear-down step every time the corresponding setup step has succeeded:

	func MyFunc() error {
		if err := SetupStep1(); err != nil {
			return err
		}
		defer TeardownStep1()

		...

		if err := SetupStep2(); err != nil {
			return err
		}
		defer TeardownStep2()

		...

		if err := SetupStep3(); err != nil {
			return err
		}
		defer TeardownStep3()

		... // Do the actual thing MyFunc() was written to do

		return nil
	}

In this example, the tear-down steps will be run in the reverse order that the defer statements were issued; but, importantly, only if the relevant setup step succeeded. If, for example, SetupStep2() returns an error, then the defer statements for TeardownStep2() and TeardownStep3() will never be issued and they will never run. Note how no independent bookkeeping of “Which setup steps have/haven’t completed successfully” is required; the defer stack is the bookkeeping device.

In my opinion, this makes for code that is both easier to read and easier to write.


Clean and powerful as this pattern is, it relies on a certain organization of the surrounding code. In particular, it requires that the control flow that conducts the setup (and which issues the defer statements for tear-down) is part of the same control flow that executes the tasks to which this setup was dedicated. In our example, this amounts to the requirement that MyFunc() is both in charge of setup and tear-down, and in charge of running the code that performs the task MyFunc() was written to perform in the first place.

But what if this isn’t so?

What if we want to write a function RunWithSetup() that performs the setup tasks, then - if all the setup tasks have completed successfully - performs a programmatically-supplied (and thus, unknown-at-compile-time) task, and finally, gracefully tears down all & only those setup steps that completed successfully?

One solution is to pass the “actual” task as a function argument (i.e., a closure):

	func RunWithSetup(actualThingToDo func()) error {
		if err := SetupStep1(); err != nil {
			return err
		}
		defer TeardownStep1()

		...

		if err := SetupStep2(); err != nil {
			return err
		}
		defer TeardownStep2()

		...

		if err := SetupStep3(); err != nil {
			return err
		}
		defer TeardownStep3()

		actualThingToDo()

		return nil
	}

Alternatively, we could write a function that takes the same kind of functional argument but instead of running anything, wraps the original function in setup / tear-down code, and returns a new function object - one which the caller can then use if/when they please:

	func WrapWithSetup(actualThingToDo func()) func() error {
		return func() error {
			if err := SetupStep1(); err != nil {
				return err
			}
			defer TeardownStep1()

			...

			if err := SetupStep2(); err != nil {
				return err
			}
			defer TeardownStep2()

			...

			if err := SetupStep3(); err != nil {
				return err
			}
			defer TeardownStep3()

			actualThingToDo()

			return nil
		}
	}

The caller can then choose whether & when to call the function generated as the return-value of WrapWithSetup() - which will then execute actualThingToDo() with proper setup and tear-down.


But what if neither of these options are available? What if, for whatever reason, the API you must satisfy is organized in such a way that the caller wants to call a setup function, and get “handed” a tear-down function (without calling it yet)? The caller will then do whatever stuff they want to do, and finally call the tear-down function they were given once they’re done.

Well, it turns out it’s not hard to “imitate” the behavior of Go’s defer in a way that makes this possible, while still preserving the key advantages of the original defer-based approach. Here’s how:2

	func PerformSetup() (teardownFunc func(), err error) {
		// Create an empty slice of functions for individual tear-down tasks
		teardownTasks := [](func()){}
		// Create the "master" tear-down function, which will be returned to caller
		teardownFunc = func() {
			// Perform tasks in teardownTasks in reverse order
			slices.Reverse(teardownTasks)
			for _, f := range teardownTasks {
				f()
			}
		}

		if err := SetupStep1(); err != nil {
			return teardownFunc, err
		}
		teardownTasks = append(teardownTasks, TeardownStep1())

		...

		if err := SetupStep2(); err != nil {
			return teardownFunc, err
		}
		teardownTasks = append(teardownTasks, TeardownStep2())

		...

		if err := SetupStep3(); err != nil {
			return teardownFunc, err
		}
		teardownTasks = append(teardownTasks, TeardownStep3())

		return teardownFunc, nil
	}

Here, we get essentially the same readability and adaptability of the original defer-based pattern, while adhering to the API specifications described above: the caller of PerformSetup() will receive the entire, “master” teardownFunc() as a return value. They can then do whatever it was that the setup was for, and call teardownFunc() (or, to be more precise, whatever variable they stored this return-value in) when they’re done. They can even defer the call of teardownFunc() in the context of their code!

As before, tear-down steps are written right next to their corresponding setup steps, providing the same readability and maintainability advantages. And as before, the only tear-down steps that will actually run will be those for which the corresponding setup step completed successfully.

The only difference is that when the API is organized this way, it’s the responsibility of the caller to check whether PerformSetup() completed successfully (and thus, returned nil in the error field) or encountered an error. The returned teardownFunc() function should be called in both scenarios; but the code for which PerformSetup() provided the necessary setup should be skipped in scenarios where PerformSetup() returned an error.

 


Footnotes:
  1. One could potentially object to this on the grounds that what we really should be doing here is separating our one object into a series of nested/encapsulated objects, each one of which involves only one setup task (in its constructor) and one tear-down task (in its destructor). Then, the very nesting/encapsulation of these objects could serve as the bookkeeping device alluded to in the main text. While this too is technically doable, it still has several drawbacks compared to Go’s defer-based approach described in the text. First, it still doesn’t produce code that is anywhere near as readable as the corresponding defer-based Go code. Second, and perhaps more important, it requires the sequence and arrangement of setup and tear-down tasks to be hard-coded as part of the object model (=what’s nested/encapsulated in what). But what if setup is dynamic, and branching, depending on conditions that cannot be determined until runtime? The defer-based Go model easily accommodates such circumstances, since a given tear-down step is pushed onto the defer stack in tandem with its setup step. A certain setup task only runs conditionally? No problem - just put both the setup step and its corresponding tear-down step defer statement inside one and the same conditional block, and voilà! Importantly, the same thing can be done with the “inverse defer pattern” that is the subject of this post, this time by placing both the setup step and its corresponding tear-down step append statement inside one and the same conditional block. 

  2. This code makes use of the slices.Reverse() function, which is part of Go’s standard library starting in Go version 1.21; for a pure Go alternative to slices.Reverse() that works on older Go versions, see this StackOverflow post (among others). But, honestly, you should probably just use the wonderful lo package instead.