I Don't Like Defer

wedg_2 pts0 comments

I don't like defer | wedg.devTable of Contents

Ok that&rsquo;s a bit of an exaggeration. It&rsquo;s not that I don&rsquo;t like defer, I think I might just prefer something else. But I&rsquo;m not sure why. In this blog post we&rsquo;ll take a look defer and its alternatives, and hopefully I&rsquo;ll come out with a better idea of my own preferences.<br>what is defer?#<br>Okay, the necessary part. What am I even talking about?<br>defer is a statement in Go that defers some function call to the end of the current function. The reason I believe defer exists is for resource cleanup. Often in programming you&rsquo;ll have some kind of value for which there&rsquo;s some necessary cleanup logic. An example of this might be a file which you have to eventually close, or a mutex you&rsquo;ll have to eventually unlock or a connection you&rsquo;ll have to eventually disconnect from.<br>Take the following example:<br>func Foo(path string) error {<br>// We open a file.<br>myFile, err := os.Open(path)<br>if err != nil {<br>return err

// Read out all the data<br>data, err := io.ReadAll(myFile)<br>if err != nil {<br>myFile.Close()<br>return err

// Call some other function that may return an error<br>err = Bar(data)<br>if err != nil {<br>myFile.Close()<br>return err

myFile.Close()<br>return nil

Since we have to close myFile eventually, we need to remember to do it at every return site. The designers of Go upon us recognised this repetition and blessed us with defer:<br>func Foo(path string) error {<br>// We open a file.<br>myFile, err := os.Open(path)<br>if err != nil {<br>return err<br>defer myFile.Close()

// Read out all the data<br>data, err := io.ReadAll(myFile)<br>if err != nil {<br>return err

// Call some other function<br>err = Bar(data)<br>if err != nil {<br>return err

return nil

And you can maybe see how, especially in larger functions, this could be helpful:<br>It reduces repetition;<br>And by reducing repetition, it means you&rsquo;re less likely to forget to close your file, since you only have to remember to write one line instead of several.<br>But you still have to remember to write defer myFile.Close() alongside your os.Open, so this design doesn&rsquo;t protect you against forgetting to write defer myFile.Close(). Let&rsquo;s take a look at another language to understand some other approaches.<br>Python&rsquo;s with#<br>Python has this thing called with. Let&rsquo;s look at a similar example:<br>def foo(path):<br># We open a file<br>with open(path) as my_file:<br># Read out all the data<br>data = my_file.read()

# Call some other function<br>bar(data)

There&rsquo;s a few things going on here that are different:<br>Python uses exceptions, which hides the error handling logic away. Go makes it explicit.<br>The file is closed at the end of the with block. No explicit .Close().<br>Now this is interesting. Python is grouping together creation and cleanup of a resource in a single construct. Unlike the Go example, you can&rsquo;t forget to close after opening, it&rsquo;s all handled by the with block.<br>Although the problem isn&rsquo;t completely solved. You can still forget to use with in the first place, which means you can still forget to close the file. For example, there&rsquo;s nothing stopping you from writing:<br>def foo(path):<br># We open a file<br>my_file = open(path)

# Call some other function<br>bar(data)

# We forget to close the file here!

This is because open(path) is a regular function that returns a regular value, so you can use it in regular assignment. Go and Python share this problem, where file values aren&rsquo;t special in any way, so you have to remember to read the documentation to know you have to treat them differently - and that&rsquo;s assuming you actually have quality documentation!<br>Let&rsquo;s keep the ball rolling and take a look at another language.<br>C++&rsquo;s &mldr;. nothing?#<br>void foo(const std::string& path) {<br>// Open the file (if = input file)<br>std::ifstream my_file(path);<br>if (!my_file) throw std::runtime_error("open failed");

// Read out all the data<br>std::stringstream buf;<br>buf my_file.rdbuf();<br>std::string data = buf.str();

// Call some other function<br>bar(data);

What is going on here? Which part of this deals with resource cleanup? Have I forgotten something?<br>Well, just like with Go and Python, my_file is just a regular value. But in C++ values have a superpower - they can define what&rsquo;s called a &lsquo;destructor&rsquo;. A destructor is code that will execute when a value reaches the end of its lifetime.<br>In the example above my_file is a local variable, so it only exists (or &rsquo;lives&rsquo;) until the end of the function. The destructor for std::ifstream closes the file at the end of our function body. This technique is called RAII (Resource-Acquisition-Is-Initialisation[^1]).<br>As C++ has this idea of destructors, it doesn&rsquo;t need any special language features like defer or with to reason about resource management. Instead it provides robust tools for reasoning with the lifetime of a value, and we can leverage that to manage resources.<br>Why don&rsquo;t Go and Python have destructors? This...

rsquo file close data defer open

Related Articles